Skip to content

Commit

Permalink
Merge branch 'main' into origin/feature/#605
Browse files Browse the repository at this point in the history
  • Loading branch information
takahirom authored Jan 28, 2025
2 parents 1364461 + 80bb882 commit f05360a
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 51 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION_NAME=1.39.0
VERSION_NAME=1.40.1
GROUP=io.github.takahirom.roborazzi
# Project-wide Gradle settings.

Expand Down
10 changes: 5 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ roborazzi-for-replacing-by-include-build = "1.0.0"

androidx-activity = "1.7.2"
androidx-appcompat = "1.7.0"
androidx-compose-material = "1.7.5"
androidx-compose-material = "1.7.6"
androidx-compose-material3 = "1.3.1"
androidx-compose-foundation = "1.7.5"
androidx-compose-runtime = "1.7.5"
androidx-compose-ui = "1.4.0"
androidx-compose-runtime = "1.7.6"
androidx-compose-ui = "1.7.6"
androidx-compose-ui-test = "1.7.6"
androidx-compose-ui-test-junit4 = "1.7.5"
androidx-compose-ui-test-manifest = "1.4.0"
androidx-compose-ui-test-junit4 = "1.7.6"
androidx-compose-ui-test-manifest = "1.7.6"
androidx-compose-ui-tooling = "1.4.0"
androidx-compose-ui-tooling-preview = "1.4.0"
androidx-constraintlayout = "2.1.4"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.github.takahirom.roborazzi

import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.semantics.AccessibilityAction
import androidx.compose.ui.semantics.CollectionInfo
import androidx.compose.ui.semantics.SemanticsConfiguration
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsProperties
Expand Down Expand Up @@ -135,6 +136,8 @@ private fun StringBuilder.appendConfigInfo(config: SemanticsConfiguration, inden
}
} else if (value is Iterable<*>) {
append(value.sortedBy { it.toString() })
} else if (value is CollectionInfo) {
append("(rowCount=${value.rowCount}, columnCount=${value.columnCount})")
} else {
append(value)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,31 @@ package com.github.takahirom.roborazzi
data class AiAssertionOptions(
val aiAssertionModel: AiAssertionModel,
val aiAssertions: List<AiAssertion> = emptyList(),
val systemPrompt: String = """Evaluate the following assertion for fulfillment in the new image.
The evaluation should be based on the comparison between the original image on the left and the new image on the right, with differences highlighted in red in the center. Focus on whether the new image fulfills the requirement specified in the user input.
val assertionImageType: AssertionImageType = AssertionImageType.Comparison(),
val systemPrompt: String = when (assertionImageType) {
is AssertionImageType.Actual -> """Evaluate the new image's fulfillment of the user's requirements.
The assessment should be based solely on the provided reference image
and the user's input specifications. Focus on whether the new image
meets all functional and design requirements.
Output:
For each assertion:
A fulfillment percentage from 0 to 100.
A brief explanation of how this percentage was determined.""",
- A fulfillment percentage from 0 to 100
- A justification based on requirement adherence rather than visual differences
"""

is AssertionImageType.Comparison -> """Evaluate the following assertion for fulfillment in the new image.
The evaluation should be based on the comparison between the original image
on the left and the new image on the right, with differences highlighted in red
in the center. Focus on whether the new image fulfills the requirement specified
in the user input.
Output:
For each assertion:
- A fulfillment percentage from 0 to 100
- A brief explanation of how this percentage was determined
"""
},
val promptTemplate: String = """Assertions:
INPUT_PROMPT
""",
Expand All @@ -33,12 +51,18 @@ INPUT_PROMPT
actualImageFilePath: String,
aiAssertionOptions: AiAssertionOptions
): AiAssertionResults

companion object {
const val DefaultMaxOutputTokens = 300
const val DefaultTemperature = 0.4F
}
}

sealed interface AssertionImageType {
class Comparison : AssertionImageType
class Actual : AssertionImageType
}

data class AiAssertion(
val assertionPrompt: String,
val failIfNotFulfilled: Boolean = true,
Expand All @@ -47,4 +71,6 @@ INPUT_PROMPT
*/
val requiredFulfillmentPercent: Int? = 80
)
}
}

class AiAssertionApiException(val statusCode: Int, message: String) : Exception(message)
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import com.github.takahirom.roborazzi.AiAssertionOptions.AiAssertionModel
import com.github.takahirom.roborazzi.AiAssertionOptions.AiAssertionModel.Companion.DefaultMaxOutputTokens
import com.github.takahirom.roborazzi.AiAssertionOptions.AiAssertionModel.Companion.DefaultTemperature
import dev.shreyaspatil.ai.client.generativeai.GenerativeModel
import dev.shreyaspatil.ai.client.generativeai.type.FunctionType
import dev.shreyaspatil.ai.client.generativeai.type.GenerationConfig
import dev.shreyaspatil.ai.client.generativeai.type.PlatformImage
import dev.shreyaspatil.ai.client.generativeai.type.Schema
import dev.shreyaspatil.ai.client.generativeai.type.content
import dev.shreyaspatil.ai.client.generativeai.type.generationConfig
import dev.shreyaspatil.ai.client.generativeai.type.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand Down Expand Up @@ -68,8 +63,12 @@ class GeminiAiAssertionModel(
val template = aiAssertionOptions.promptTemplate

val inputPrompt = aiAssertionOptions.inputPrompt(aiAssertionOptions)
val imageFilePath = when (aiAssertionOptions.assertionImageType) {
is AiAssertionOptions.AssertionImageType.Comparison -> comparisonImageFilePath
is AiAssertionOptions.AssertionImageType.Actual -> actualImageFilePath
}
val inputContent = content {
image(readByteArrayFromFile(comparisonImageFilePath))
image(readByteArrayFromFile(imageFilePath))
val prompt = template.replace("INPUT_PROMPT", inputPrompt)
text(prompt)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,15 @@ package com.github.takahirom.roborazzi
import com.github.takahirom.roborazzi.AiAssertionOptions.AiAssertionModel.Companion.DefaultMaxOutputTokens
import com.github.takahirom.roborazzi.AiAssertionOptions.AiAssertionModel.Companion.DefaultTemperature
import com.github.takahirom.roborazzi.CaptureResults.Companion.json
import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.HttpTimeout.Plugin.INFINITE_TIMEOUT_MS
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.logging.SIMPLE
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.runBlocking
import kotlinx.io.buffered
import kotlinx.io.files.Path
Expand Down Expand Up @@ -57,9 +49,9 @@ class OpenAiAiAssertionModel(
}
if (loggingEnabled) {
install(Logging) {
logger = object: Logger {
logger = object : Logger {
override fun log(message: String) {
Logger.SIMPLE.log(message.replace(apiKey, "****"))
Logger.SIMPLE.log(message.hideApiKey(apiKey))
}
}
level = LogLevel.ALL
Expand All @@ -77,7 +69,11 @@ class OpenAiAiAssertionModel(
val systemPrompt = aiAssertionOptions.systemPrompt
val template = aiAssertionOptions.promptTemplate
val inputPrompt = aiAssertionOptions.inputPrompt(aiAssertionOptions)
val imageBytes = readByteArrayFromFile(comparisonImageFilePath)
val imageFilePath = when (aiAssertionOptions.assertionImageType) {
is AiAssertionOptions.AssertionImageType.Comparison -> comparisonImageFilePath
is AiAssertionOptions.AssertionImageType.Actual -> actualImageFilePath
}
val imageBytes = readByteArrayFromFile(imageFilePath)
val imageBase64 = imageBytes.encodeBase64()
val messages = listOf(
Message(
Expand Down Expand Up @@ -141,7 +137,13 @@ class OpenAiAiAssertionModel(
setBody(requestBody)
}
val bodyText = response.bodyAsText()
debugLog { "OpenAiAiModel: response: $bodyText" }
debugLog { "OpenAiAiModel: response: ${bodyText.hideApiKey(apiKey)}" }
if (response.status.value >= 400) {
throw AiAssertionApiException(
response.status.value, bodyText
.hideApiKey(apiKey)
)
}
val responseBody: ChatCompletionResponse = json.decodeFromString(bodyText)
return responseBody.choices.firstOrNull()?.message?.content ?: ""
}
Expand Down Expand Up @@ -305,4 +307,8 @@ private data class OpenAiConditionResult(
@SerialName("fulfillment_percent")
val fulfillmentPercent: Int,
val explanation: String?,
)
)

private fun String.hideApiKey(key: String): String {
return this.replace(key.ifBlank { "****" }, "****")
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import android.app.Activity
import android.graphics.Color
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.Dump
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.RoborazziComposeActivityScenarioOption
import com.github.takahirom.roborazzi.RoborazziComposeComposableOption
import com.github.takahirom.roborazzi.RoborazziComposeOptions
import com.github.takahirom.roborazzi.RoborazziOptions
import com.github.takahirom.roborazzi.activityTheme
import com.github.takahirom.roborazzi.captureRoboImage
import com.github.takahirom.roborazzi.fontScale
Expand All @@ -40,6 +43,19 @@ class ComposeLambdaTest {
}
}

@OptIn(ExperimentalRoborazziApi::class)
@Test
fun captureComposeCollection() {
captureRoboImage(
filePath = "${roborazziSystemPropertyOutputDirectory()}/manual_compose_collection.png",
roborazziOptions = RoborazziOptions(captureType = RoborazziOptions.CaptureType.Dump())
) {
Row {
listOf("Hello", "Compose!").forEach { Text(it) }
}
}
}

@OptIn(ExperimentalRoborazziApi::class)
@Test
fun whenNonTransparentThemeItShouldHaveNonTransparentBackground() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.AiAssertionOptions
import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
import com.github.takahirom.roborazzi.OpenAiAiAssertionModel
import com.github.takahirom.roborazzi.ROBORAZZI_DEBUG
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.RoborazziOptions
import com.github.takahirom.roborazzi.RoborazziRule
import com.github.takahirom.roborazzi.RoborazziTaskType
import com.github.takahirom.roborazzi.captureRoboImage
import com.github.takahirom.roborazzi.provideRoborazziContext
import com.github.takahirom.roborazzi.roboOutputName
import com.github.takahirom.roborazzi.*
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand Down

1 comment on commit f05360a

@sergio-sastre
Copy link
Contributor

@sergio-sastre sergio-sastre commented on f05360a Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I‘ve been busy lately, and I‘m currently working on 0.5.1 to provide support for repeated annotations.

I‘m afraid I won‘t be able to work on this till beginning of next week at least.

My initial idea is to create in ComposablePreviewScanner 0.5.1 a method getRepeatedAnnotation<ManualRoboCapture>(): List<ManualRoboCapture> and that should be used in Roborazzi to run in a loop „advanceTime + captureRoboImage“ for all the annotations that getRepeatedAnnotation() returns.

This also means, the screenshot file names should also consider that it took several screenshots, maybe with an index

Please sign in to comment.