Merge "Remove Paparazzi from androidx" into androidx-main
diff --git a/buildSrc/apply/applyAndroidXPaparazziImplPlugin.gradle b/buildSrc/apply/applyAndroidXPaparazziImplPlugin.gradle
deleted file mode 100644
index ece0f79..0000000
--- a/buildSrc/apply/applyAndroidXPaparazziImplPlugin.gradle
+++ /dev/null
@@ -1,9 +0,0 @@
-import androidx.build.paparazzi.AndroidXPaparazziImplPlugin
-
-buildscript {
- dependencies {
- classpath(project.files("${project.rootProject.ext["buildSrcOut"]}/private/build/libs/private.jar"))
- }
-}
-
-apply plugin: AndroidXPaparazziImplPlugin
diff --git a/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXPaparazziPlugin.kt b/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXPaparazziPlugin.kt
deleted file mode 100644
index 090d9b6..0000000
--- a/buildSrc/plugins/src/main/kotlin/androidx/build/AndroidXPaparazziPlugin.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build
-
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-
-/**
- * Configures screenshot testing using Paparazzi for AndroidX projects.
- *
- * The actual implementation is in AndroidXPaparazziImplPlugin.
- */
-class AndroidXPaparazziPlugin : Plugin<Project> {
- override fun apply(project: Project) {
- val supportRoot = project.getSupportRootFolder()
- project.apply(
- mapOf("from" to "$supportRoot/buildSrc/apply/applyAndroidXPaparazziImplPlugin.gradle")
- )
- }
-}
diff --git a/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXPaparazziPlugin.properties b/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXPaparazziPlugin.properties
deleted file mode 100644
index 872551c..0000000
--- a/buildSrc/plugins/src/main/resources/META-INF/gradle-plugins/AndroidXPaparazziPlugin.properties
+++ /dev/null
@@ -1,17 +0,0 @@
-#
-# Copyright 2022 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-implementation-class=androidx.build.AndroidXPaparazziPlugin
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
deleted file mode 100644
index 6c3b775..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build.paparazzi
-
-import androidx.build.OperatingSystem
-import androidx.build.defaultAndroidConfig
-import androidx.build.getLibraryByName
-import androidx.build.getOperatingSystem
-import androidx.build.getSdkPath
-import androidx.build.getSupportRootFolder
-import com.android.build.gradle.BaseExtension
-import javax.inject.Inject
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-import org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE
-import org.gradle.api.artifacts.type.ArtifactTypeDefinition.JAR_TYPE
-import org.gradle.api.file.Directory
-import org.gradle.api.file.FileCollection
-import org.gradle.api.file.FileSystemOperations
-import org.gradle.api.provider.Provider
-import org.gradle.api.tasks.Copy
-import org.gradle.api.tasks.PathSensitivity
-import org.gradle.api.tasks.testing.Test
-import org.gradle.kotlin.dsl.get
-import org.gradle.kotlin.dsl.register
-import org.gradle.kotlin.dsl.the
-import org.gradle.kotlin.dsl.withType
-import org.gradle.process.JavaForkOptions
-
-/** Configures screenshot testing using Paparazzi for AndroidX projects. */
-class AndroidXPaparazziImplPlugin
-@Inject
-constructor(private val fileSystemOperations: FileSystemOperations) : Plugin<Project> {
- override fun apply(project: Project) {
- val paparazziNative = project.createUnzippedPaparazziNativeDependency()
- project.afterEvaluate { it.addTestUtilsDependency() }
- project.tasks.register("updateGolden")
- project.afterEvaluate {
- // need to be inside of afterEvaluate because we read android.namespace
- // ideally, we refactor to use a lazy API
- project.tasks.withType<Test>().configureEach { it.configureTestTask(paparazziNative) }
- }
- project.tasks.withType<Test>().whenTaskAdded { project.registerUpdateGoldenTask(it) }
- }
-
- /**
- * Add project's golden directory and the unzipped native Paparazzi location as task inputs, and
- * set system properties for the test library to consume at runtime.
- */
- private fun Test.configureTestTask(paparazziNative: FileCollection) {
- val compileSdkVersion = project.defaultAndroidConfig.compileSdk
- val platformDirectory = project.getSdkPath().resolve("platforms/$compileSdkVersion")
- val cachedGoldenRootDirectory = project.goldenRootDirectory
- val cachedReportDirectory = reportDirectory
- val android = project.the<BaseExtension>()
- val packageName =
- requireNotNull(android.namespace) { "android.namespace must be set for Paparazzi" }
-
- // Attach unzipped Paparazzi native directory as a task input
- inputs
- .files(paparazziNative)
- .withPathSensitivity(PathSensitivity.NONE)
- .withPropertyName("paparazziNative")
-
- // Attach golden directory to task inputs to invalidate tests when updating goldens
- inputs
- .dir(project.goldenDirectory)
- .withPathSensitivity(PathSensitivity.RELATIVE)
- .withPropertyName("goldenDirectory")
-
- // Mark report directory as an output directory
- outputs.dir(reportDirectory).withPropertyName("paparazziReportDir")
-
- // Clean the contents of the report directory before each test run
- doFirst {
- fileSystemOperations.delete {
- it.delete(cachedReportDirectory.get().asFile.listFiles())
- }
- }
-
- // Set non-path system properties at configuration time, so that changes invalidate caching
- prefixedSystemProperties(
- "gradlePluginApplied" to "true",
- "compileSdkVersion" to project.defaultAndroidConfig.targetSdk,
- "resourcePackageNames" to packageName, // TODO: Transitive resource packages?
- "modulePath" to project.modulePath,
- "updateGoldenTask" to "${project.path}:${updateGoldenTaskName()}"
- )
-
- // Set the remaining system properties at execution time, after the snapshotting, so that
- // the absolute paths don't affect caching
- doFirst {
- systemProperty("paparazzi.platform.data.root", paparazziNative.singleFile.canonicalPath)
- prefixedSystemProperties(
- "platformDir" to platformDirectory.canonicalPath,
- "assetsDir" to ".", // TODO: Merged assets dirs? (needed for compose?)
- "resDir" to ".", // TODO: Merged resource dirs? (needed for compose?)
- "reportDir" to cachedReportDirectory.get().asFile.canonicalPath,
- "goldenRootDir" to cachedGoldenRootDirectory.canonicalPath,
- )
- }
- }
-
- /** Register a copy task for moving new images to the golden directory. */
- private fun Project.registerUpdateGoldenTask(testTask: Test) {
- tasks.register<Copy>(testTask.updateGoldenTaskName()) {
- dependsOn(testTask)
-
- from(testTask.reportDirectory) {
- include("**/*_actual.png")
- into(goldenDirectory)
- rename { it.removeSuffix("_actual.png") + "_paparazzi.png" }
- }
- }
-
- tasks["updateGolden"].dependsOn(testTask.updateGoldenTaskName())
- }
-
- /** Derive updateGolden task name from a test task name. */
- private fun Test.updateGoldenTaskName(): String {
- return "updateGolden" + name.removePrefix("test").replaceFirstChar { it.titlecase() }
- }
-
- /**
- * Configure [UnzipPaparazziNativeTransform] for the project, and add the platform-specific
- * Paparazzi native layoutlib dependency, using the version in `libs.versions.toml`.
- */
- private fun Project.createUnzippedPaparazziNativeDependency(): FileCollection {
- val platformSuffix =
- when (val os = getOperatingSystem()) {
- OperatingSystem.LINUX -> "LinuxX64"
- OperatingSystem.MAC -> {
- val arch = System.getProperty("os.arch")
- if (arch.startsWith("x86", ignoreCase = true)) "MacOsX64" else "MacOsArm64"
- }
- else -> error("Unsupported operating system $os for Paparazzi")
- }
-
- dependencies.registerTransform(UnzipPaparazziNativeTransform::class.java) { spec ->
- spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, JAR_TYPE)
- spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, UNZIPPED_PAPARAZZI_NATIVE)
- }
-
- val configuration = configurations.create("paparazziNative")
- configuration.isCanBeConsumed = false
- configuration.dependencies.add(
- dependencies.create(getLibraryByName("paparazziNative$platformSuffix"))
- )
-
- return configuration.incoming
- .artifactView {
- it.attributes.attribute(ARTIFACT_TYPE_ATTRIBUTE, UNZIPPED_PAPARAZZI_NATIVE)
- }
- .files
- }
-
- /** The golden image directory for this project. */
- private val Project.goldenDirectory
- get() = goldenRootDirectory.resolve(modulePath)
-
- /** The root of the golden image directory in a standard AndroidX checkout. */
- private val Project.goldenRootDirectory
- get() = getSupportRootFolder().resolve("../../golden")
-
- /** Filesystem path for this module derived from Gradle project path. */
- private val Project.modulePath
- get() = path.replace(':', '/').trim('/')
-
- /** Output directory for storing reports and images. */
- private val Test.reportDirectory: Provider<Directory>
- get() = project.layout.buildDirectory.dir("paparazzi/$name")
-
- /** Add a testImplementation dependency on the wrapper test utils library. */
- private fun Project.addTestUtilsDependency() {
- configurations["testImplementation"]
- .dependencies
- .add(dependencies.create(project(TEST_UTILS_PROJECT)))
- }
-
- private companion object {
- /** Package name of the test library, used to namespace system properties */
- const val PACKAGE_NAME = "androidx.testutils.paparazzi"
-
- /** Project path to the wrapper test utils project. */
- const val TEST_UTILS_PROJECT = ":internal-testutils-paparazzi"
-
- /** Artifact type attribute for unzipped Paparazzi layoutlib unzipped artifacts */
- const val UNZIPPED_PAPARAZZI_NATIVE = "unzipped-paparazzi-native"
-
- /** Set system properties with keys prefixed with [PACKAGE_NAME] */
- fun JavaForkOptions.prefixedSystemProperties(vararg properties: Pair<String, Any>) {
- properties.forEach { (name, value) -> systemProperty("$PACKAGE_NAME.$name", value) }
- }
- }
-}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/UnzipPaparazziNativeTransform.kt b/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/UnzipPaparazziNativeTransform.kt
deleted file mode 100644
index 2d2c09a..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/UnzipPaparazziNativeTransform.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build.paparazzi
-
-import java.util.zip.ZipInputStream
-import org.gradle.api.artifacts.transform.InputArtifact
-import org.gradle.api.artifacts.transform.TransformAction
-import org.gradle.api.artifacts.transform.TransformOutputs
-import org.gradle.api.artifacts.transform.TransformParameters.None
-import org.gradle.api.file.FileSystemLocation
-import org.gradle.api.provider.Provider
-import org.gradle.api.tasks.PathSensitive
-import org.gradle.api.tasks.PathSensitivity
-import org.gradle.work.DisableCachingByDefault
-
-/**
- * Unzips one of Paparazzi's platform-specific layoutlib JAR artifacts so that Paparazzi can read
- * its contents at run time. These contain a native dynamic library and supporting resources
- * including ICU and fonts.
- */
-@DisableCachingByDefault(because = "Just an unzip task, faster to rerun locally")
-abstract class UnzipPaparazziNativeTransform : TransformAction<None> {
- @get:PathSensitive(PathSensitivity.NAME_ONLY)
- @get:InputArtifact
- abstract val primaryInput: Provider<FileSystemLocation>
-
- override fun transform(outputs: TransformOutputs) {
- val inputFile = primaryInput.get().asFile
- val outputDir = outputs.dir(inputFile.nameWithoutExtension).also { it.mkdirs() }
-
- ZipInputStream(inputFile.inputStream().buffered()).use { zipInputStream ->
- while (true) {
- val entry = zipInputStream.nextEntry ?: break
- val outputFile = outputDir.resolve(entry.name)
-
- if (entry.isDirectory) {
- outputFile.mkdir()
- } else {
- // This works because ZipInputStream resizes itself to the contents of the
- // last-returned entry
- outputFile.outputStream().buffered().use { zipInputStream.copyTo(it) }
- }
- }
- }
- }
-}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/transform/ConfigureAarAsJar.kt b/buildSrc/private/src/main/kotlin/androidx/build/transform/ConfigureAarAsJar.kt
index b6c9910..877109a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/transform/ConfigureAarAsJar.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/transform/ConfigureAarAsJar.kt
@@ -63,8 +63,6 @@
.dependencies
.add(project.dependencies.create(aarAsJar))
- // Added to allow the :external:paparazzi:paparazzi build to select the correct jar (not get
- // confused by the mpp jars) when the mpp builds are enabled
project.configurations
.getByName(testAarsAsJars.name)
.attributes
diff --git a/buildSrc/private/src/main/resources/META-INF/gradle-plugins/AndroidXPaparazziImplPlugin.properties b/buildSrc/private/src/main/resources/META-INF/gradle-plugins/AndroidXPaparazziImplPlugin.properties
deleted file mode 100644
index 0ea8b07..0000000
--- a/buildSrc/private/src/main/resources/META-INF/gradle-plugins/AndroidXPaparazziImplPlugin.properties
+++ /dev/null
@@ -1,17 +0,0 @@
-#
-# Copyright 2022 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-implementation-class=androidx.build.paparazzi.AndroidXPaparazziImplPlugin
diff --git a/buildSrc/settingsScripts/project-dependency-graph.groovy b/buildSrc/settingsScripts/project-dependency-graph.groovy
index d74e0d8..34e95d3 100644
--- a/buildSrc/settingsScripts/project-dependency-graph.groovy
+++ b/buildSrc/settingsScripts/project-dependency-graph.groovy
@@ -239,10 +239,6 @@
links.add(":compose:compiler:compiler")
links.add(":compose:lint:internal-lint-checks")
}
- if (paparazziPlugin.matcher(line).find()) {
- links.add(":test:screenshot:screenshot-proto")
- links.add(":internal-testutils-paparazzi")
- }
if (iconGenerator.matcher(line).find()) {
links.add(":compose:material:material:icons:generator")
}
@@ -267,7 +263,6 @@
private static Pattern multilineProjectReference = Pattern.compile("project\\(\$")
private static Pattern inspection = Pattern.compile("packageInspector\\(project, \"(.*)\"\\)")
private static Pattern composePlugin = Pattern.compile("id\\(\"AndroidXComposePlugin\"\\)")
- private static Pattern paparazziPlugin = Pattern.compile("id\\(\"AndroidXPaparazziPlugin\"\\)")
private static Pattern iconGenerator = Pattern.compile("IconGenerationTask\\.register")
}
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 29b6d1b..ab1f2c9 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -28,7 +28,6 @@
id("AndroidXPlugin")
id("com.android.library")
id("AndroidXComposePlugin")
- id("AndroidXPaparazziPlugin")
}
@@ -122,8 +121,8 @@
implementation(libs.junit)
implementation(libs.truth)
implementation(libs.kotlinReflect)
- implementation(libs.mockitoCore)
- implementation(libs.mockitoKotlin)
+ implementation(libs.mockitoCore4)
+ implementation(libs.mockitoKotlin4)
implementation(project(":constraintlayout:constraintlayout-compose"))
}
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.android.kt
index 968f6ec..700bbca 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/ToCharArray.android.kt
@@ -16,7 +16,6 @@
package androidx.compose.foundation.text.input.internal
-import android.text.TextUtils
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.text.input.TextFieldCharSequence
import androidx.compose.foundation.text.input.toCharArray
@@ -31,6 +30,9 @@
if (this is TextFieldCharSequence) {
toCharArray(destination, destinationOffset, startIndex, endIndex)
} else {
- TextUtils.getChars(this, startIndex, endIndex, destination, destinationOffset)
+ var dstIndex = destinationOffset
+ for (srcIndex in startIndex until endIndex) {
+ destination[dstIndex++] = this[srcIndex]
+ }
}
}
diff --git a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/BasicTextPaparazziTest.kt b/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/BasicTextPaparazziTest.kt
deleted file mode 100644
index c65c711..0000000
--- a/compose/foundation/foundation/src/androidUnitTest/kotlin/androidx/compose/foundation/text/BasicTextPaparazziTest.kt
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.text
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.constraintlayout.compose.ConstraintLayout
-import androidx.constraintlayout.compose.Dimension
-import androidx.testutils.paparazzi.androidxPaparazzi
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class BasicTextPaparazziTest {
- @get:Rule
- val paparazzi = androidxPaparazzi()
-
- @Test
- fun colorChangingState_changesColor() {
- paparazzi.snapshot {
- var color = remember { mutableStateOf(Color.Red) }
- BasicText(
- "ABCD",
- color = { color.value }
- )
- SideEffect {
- color.value = Color.Yellow
- }
- }
- }
-
- @Test
- fun colorChangingState_changesColor_annotatedString() {
- paparazzi.snapshot {
- var color = remember { mutableStateOf(Color.Red) }
- BasicText(
- AnnotatedString("ABCD"),
- color = { color.value }
- )
- SideEffect {
- color.value = Color.Yellow
- }
- }
- }
-
- @Test
- fun overflowEllipsis_doesEllipsis_whenInPreferredWrapContent() {
- paparazzi.snapshot {
- // b/275369323
- ConstraintLayout(modifier = Modifier
- .width(100.dp)
- .background(Color.White)
- .padding(8.dp)) {
- val (thumbnail, textBox, actionButton) = createRefs()
- Box(modifier = Modifier
- .background(Color.Green)
- .constrainAs(thumbnail) {
- top.linkTo(parent.top)
- start.linkTo(parent.start)
- bottom.linkTo(parent.bottom)
- }
- .size(28.dp)
- )
- // Text region
- Column(
- modifier = Modifier
- .constrainAs(textBox) {
- top.linkTo(parent.top)
- bottom.linkTo(parent.bottom)
- start.linkTo(thumbnail.end)
- end.linkTo(actionButton.start)
- width = Dimension.preferredWrapContent
- },
- ) {
- BasicText(
- text = "ASome very long text that is sure to clip in this layout",
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- }
- Box(modifier = Modifier
- .constrainAs(actionButton) {
- top.linkTo(parent.top)
- bottom.linkTo(parent.bottom)
- end.linkTo(parent.end)
- }
- .background(Color.Blue.copy(alpha = 0.5f))
- .size(28.dp)
- )
- }
- }
- }
-
- @Test
- fun RtlAppliedCorrectly_inConstraintLayout_withWrapContentText() {
- // b/275369323
- paparazzi.snapshot {
- CompositionLocalProvider(
- LocalLayoutDirection provides LayoutDirection.Rtl
- ) {
- ConstraintLayout(
- Modifier
- .fillMaxWidth()
- .background(Color.Green)) {
- val (title, progressBar, expander) = createRefs()
- BasicText(
- text = "Locale-aware Text",
- modifier = Modifier
- .constrainAs(title) {
- top.linkTo(parent.top)
- start.linkTo(parent.start)
- end.linkTo(expander.start)
- width = Dimension.fillToConstraints
- }
- .border(2.dp, Color.Red)
- )
- Box(
- modifier = Modifier
- .constrainAs(progressBar) {
- top.linkTo(title.bottom)
- start.linkTo(parent.start)
- end.linkTo(expander.start)
- width = Dimension.fillToConstraints
- height = Dimension.value(10.dp)
- }
- .background(Color.Yellow)
- )
- // expander image button
- Box(modifier = Modifier
- .constrainAs(expander) {
- top.linkTo(parent.top)
- start.linkTo(progressBar.end)
- end.linkTo(parent.end)
- width = Dimension.value(28.dp)
- height = Dimension.value(28.dp)
- }
- .background(Color.Cyan)
- )
- }
- }
- }
- }
-}
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 3a83ec4..21f35fb2 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -28,7 +28,6 @@
id("AndroidXPlugin")
id("com.android.library")
id("AndroidXComposePlugin")
- id("AndroidXPaparazziPlugin")
}
androidXMultiplatform {
diff --git a/compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/ButtonPaparazziScreenshotTest.kt b/compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/ButtonPaparazziScreenshotTest.kt
deleted file mode 100644
index 1aec7be..0000000
--- a/compose/material/material/src/androidUnitTest/kotlin/androidx/compose/material/ButtonPaparazziScreenshotTest.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material
-
-import androidx.testutils.paparazzi.androidxPaparazzi
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class ButtonPaparazziScreenshotTest {
- @get:Rule
- val paparazzi = androidxPaparazzi()
-
- @Test
- fun default_button() {
- paparazzi.snapshot {
- MaterialTheme {
- Surface {
- Button(onClick = { }) {
- Text("Button")
- }
- }
- }
- }
- }
-}
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 293af59..da3e1db 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -28,7 +28,6 @@
id("AndroidXPlugin")
id("com.android.library")
id("AndroidXComposePlugin")
- id("AndroidXPaparazziPlugin")
}
androidXMultiplatform {
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/ButtonPaparazziTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/ButtonPaparazziTest.kt
deleted file mode 100644
index e386d4e..0000000
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/ButtonPaparazziTest.kt
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.material3
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.requiredSize
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Favorite
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.unit.dp
-import androidx.testutils.paparazzi.androidxPaparazzi
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class ButtonPaparazziTest {
- @get:Rule
- val paparazzi = androidxPaparazzi()
-
- @Test
- fun default_button_light_theme() {
- paparazzi.snapshot {
- MaterialTheme(lightColorScheme()) {
- Surface {
- Button(onClick = { }) {
- Text("Button")
- }
- }
- }
- }
- }
-
- @Test
- fun default_button_dark_theme() {
- paparazzi.snapshot {
- MaterialTheme(darkColorScheme()) {
- Surface {
- Button(onClick = { }) {
- Text("Button")
- }
- }
- }
- }
- }
-
- @Test
- fun disabled_button_light_theme() {
- paparazzi.snapshot {
- MaterialTheme(lightColorScheme()) {
- Surface {
- Button(onClick = { }, enabled = false) {
- Text("Button")
- }
- }
- }
- }
- }
-
- @Test
- fun disabled_button_dark_theme() {
- paparazzi.snapshot {
- MaterialTheme(darkColorScheme()) {
- Surface {
- Button(onClick = { }, enabled = false) {
- Text("Button")
- }
- }
- }
- }
- }
-
- @Test
- fun elevated_button_light_theme() {
- paparazzi.snapshot {
- MaterialTheme(lightColorScheme()) {
- Surface {
- Box(
- Modifier.requiredSize(
- 200.dp,
- 100.dp
- ).wrapContentSize().testTag("elevated button")
- ) {
- ElevatedButton(onClick = {}) { Text("Elevated Button") }
- }
- }
- }
- }
- }
-
- @Test
- fun elevated_button_dark_theme() {
- paparazzi.snapshot {
- MaterialTheme(darkColorScheme()) {
- Surface {
- Box(
- Modifier.requiredSize(
- 200.dp,
- 100.dp
- ).wrapContentSize().testTag("elevated button")
- ) {
- ElevatedButton(onClick = {}) { Text("Elevated Button") }
- }
- }
- }
- }
- }
-
- @Test
- fun disabled_elevated_button_light_theme() {
- paparazzi.snapshot {
- MaterialTheme(lightColorScheme()) {
- Surface {
- Box(
- Modifier.requiredSize(
- 200.dp,
- 100.dp
- ).wrapContentSize().testTag("elevated button")
- ) {
- ElevatedButton(onClick = {}, enabled = false) { Text("Elevated Button") }
- }
- }
- }
- }
- }
-
- @Test
- fun disabled_elevated_button_dark_theme() {
- paparazzi.snapshot {
- MaterialTheme(darkColorScheme()) {
- Surface {
- Box(
- Modifier.requiredSize(
- 200.dp,
- 100.dp
- ).wrapContentSize().testTag("elevated button")
- ) {
- ElevatedButton(onClick = {}, enabled = false) { Text("Elevated Button") }
- }
- }
- }
- }
- }
-
- @Test
- fun button_with_icon_light_theme() {
- paparazzi.snapshot {
- MaterialTheme(lightColorScheme()) {
- Surface {
- Button(
- onClick = { /* Do something! */ },
- contentPadding = ButtonDefaults.ButtonWithIconContentPadding
- ) {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Localized description",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- Text("Like")
- }
- }
- }
- }
- }
-
- @Test
- fun button_with_icon_dark_theme() {
- paparazzi.snapshot {
- MaterialTheme(darkColorScheme()) {
- Surface {
- Button(
- onClick = { /* Do something! */ },
- contentPadding = ButtonDefaults.ButtonWithIconContentPadding
- ) {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Localized description",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- Text("Like")
- }
- }
- }
- }
- }
-
- @Test
- fun disabled_button_with_icon_light_theme() {
- paparazzi.snapshot {
- MaterialTheme(lightColorScheme()) {
- Surface {
- Button(
- onClick = { /* Do something! */ },
- contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
- enabled = false,
- ) {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Localized description",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- Text("Like")
- }
- }
- }
- }
- }
-
- @Test
- fun disabled_button_with_icon_dark_theme() {
- paparazzi.snapshot {
- MaterialTheme(darkColorScheme()) {
- Surface {
- Button(
- onClick = { /* Do something! */ },
- contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
- enabled = false,
- ) {
- Icon(
- Icons.Filled.Favorite,
- contentDescription = "Localized description",
- modifier = Modifier.size(ButtonDefaults.IconSize)
- )
- Spacer(Modifier.size(ButtonDefaults.IconSpacing))
- Text("Like")
- }
- }
- }
- }
- }
-}
diff --git a/external/paparazzi/paparazzi-agent/build.gradle b/external/paparazzi/paparazzi-agent/build.gradle
deleted file mode 100644
index 802d417..0000000
--- a/external/paparazzi/paparazzi-agent/build.gradle
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * This file was created using the `create_project.py` script located in the
- * `<AndroidX root>/development/project-creator` directory.
- *
- * Please use that script when creating a new project, rather than copying an existing project and
- * modifying its settings.
- */
-import androidx.build.LibraryType
-
-plugins {
- id("AndroidXPlugin")
- id("kotlin")
-}
-
-dependencies {
- api(libs.kotlinStdlib)
- api(libs.junit)
- implementation(libs.byteBuddy)
- implementation(libs.byteBuddyAgent)
- testImplementation(libs.assertj)
-}
-
-androidx {
- name = "Paparazzi Agent - AndroidX Fork"
- type = LibraryType.INTERNAL_HOST_TEST_LIBRARY
-}
diff --git a/external/paparazzi/paparazzi-agent/src/main/java/app/cash/paparazzi/agent/AgentTestRule.kt b/external/paparazzi/paparazzi-agent/src/main/java/app/cash/paparazzi/agent/AgentTestRule.kt
deleted file mode 100644
index b79b5d5..0000000
--- a/external/paparazzi/paparazzi-agent/src/main/java/app/cash/paparazzi/agent/AgentTestRule.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package app.cash.paparazzi.agent
-
-import net.bytebuddy.agent.ByteBuddyAgent
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
-
-class AgentTestRule : TestRule {
- override fun apply(
- base: Statement,
- description: Description
- ) = object : Statement() {
- override fun evaluate() {
- ByteBuddyAgent.install()
- InterceptorRegistrar.registerMethodInterceptors()
- // interceptors are statically retained until test process finishes, so no need to cleanup
- base.evaluate()
- }
- }
-}
diff --git a/external/paparazzi/paparazzi-agent/src/main/java/app/cash/paparazzi/agent/InterceptorRegistrar.kt b/external/paparazzi/paparazzi-agent/src/main/java/app/cash/paparazzi/agent/InterceptorRegistrar.kt
deleted file mode 100644
index b436835..0000000
--- a/external/paparazzi/paparazzi-agent/src/main/java/app/cash/paparazzi/agent/InterceptorRegistrar.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package app.cash.paparazzi.agent
-
-import net.bytebuddy.ByteBuddy
-import net.bytebuddy.dynamic.loading.ClassReloadingStrategy
-import net.bytebuddy.implementation.MethodDelegation
-import net.bytebuddy.matcher.ElementMatchers
-
-object InterceptorRegistrar {
- private val byteBuddy = ByteBuddy()
- private val methodInterceptors = mutableListOf<() -> Unit>()
-
- fun addMethodInterceptor(
- receiver: Class<*>,
- methodName: String,
- interceptor: Class<*>
- ) = addMethodInterceptors(receiver, setOf(methodName to interceptor))
-
- fun addMethodInterceptors(
- receiver: Class<*>,
- methodNamesToInterceptors: Set<Pair<String, Class<*>>>
- ) {
- methodInterceptors += {
- var builder = byteBuddy
- .redefine(receiver)
-
- methodNamesToInterceptors.forEach {
- builder = builder
- .method(ElementMatchers.named(it.first))
- .intercept(MethodDelegation.to(it.second))
- }
-
- builder
- .make()
- .load(receiver.classLoader, ClassReloadingStrategy.fromInstalledAgent())
- }
- }
-
- fun registerMethodInterceptors() {
- methodInterceptors.forEach { it.invoke() }
- }
-
- fun clearMethodInterceptors() {
- methodInterceptors.clear()
- }
-}
diff --git a/external/paparazzi/paparazzi-agent/src/test/java/app/cash/paparazzi/agent/InterceptorRegistrarTest.kt b/external/paparazzi/paparazzi-agent/src/test/java/app/cash/paparazzi/agent/InterceptorRegistrarTest.kt
deleted file mode 100644
index 1113073..0000000
--- a/external/paparazzi/paparazzi-agent/src/test/java/app/cash/paparazzi/agent/InterceptorRegistrarTest.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package app.cash.paparazzi.agent
-
-import net.bytebuddy.agent.ByteBuddyAgent
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-
-class InterceptorRegistrarTest {
- @Before
- fun setup() {
- InterceptorRegistrar.addMethodInterceptors(
- Utils::class.java,
- setOf(
- "log1" to Interceptor1::class.java,
- "log2" to Interceptor2::class.java
- )
- )
-
- ByteBuddyAgent.install()
- InterceptorRegistrar.registerMethodInterceptors()
- }
-
- @Test
- fun test() {
- Utils.log1()
- Utils.log2()
-
- assertThat(logs).containsExactly("intercept1", "intercept2")
- }
-
- @After
- fun teardown() {
- InterceptorRegistrar.clearMethodInterceptors()
- }
-
- object Utils {
- fun log1() {
- logs += "original1"
- }
-
- fun log2() {
- logs += "original2"
- }
- }
-
- object Interceptor1 {
- @Suppress("unused")
- @JvmStatic
- fun intercept() {
- logs += "intercept1"
- }
- }
-
- object Interceptor2 {
- @Suppress("unused")
- @JvmStatic
- fun intercept() {
- logs += "intercept2"
- }
- }
-
- companion object {
- private val logs = mutableListOf<String>()
- }
-}
diff --git a/external/paparazzi/paparazzi/build.gradle b/external/paparazzi/paparazzi/build.gradle
deleted file mode 100644
index bc018ac..0000000
--- a/external/paparazzi/paparazzi/build.gradle
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * This file was created using the `create_project.py` script located in the
- * `<AndroidX root>/development/project-creator` directory.
- *
- * Please use that script when creating a new project, rather than copying an existing project and
- * modifying its settings.
- */
-import androidx.build.LibraryType
-import org.gradle.api.artifacts.transform.TransformParameters.None
-import java.util.zip.ZipInputStream
-
-plugins {
- id("AndroidXPlugin")
- id("kotlin")
- id("com.google.devtools.ksp")
- id("AndroidXComposePlugin")
-}
-
-androidx.configureAarAsJarForConfiguration("compileOnly")
-androidx.configureAarAsJarForConfiguration("testImplementation")
-
-dependencies {
- api("androidx.annotation:annotation:1.3.0")
- api("com.android.tools.layoutlib:layoutlib-api:27.2.2")
- api("com.android.tools:common:27.1.2")
- api(libs.androidToolsNinepatch)
- api("com.android.tools:sdk-common:26.6.4")
- api(libs.guava)
- api(libs.junit)
- api(libs.kotlinStdlib)
- api(libs.kotlinCoroutinesCore)
- api(libs.kxml2)
- api(libs.okio)
- api(libs.paparazziNativeJvm)
- constraints {
- implementation(libs.kotlinReflect) {
- because("sdk-common depends on an old kotlin-reflect")
- }
- }
-
- implementation(project(":external:paparazzi:paparazzi-agent"))
- implementation(libs.jcodec)
- implementation(libs.jcodecJavaSe)
- implementation(libs.moshi)
- implementation(libs.moshiAdapters)
-
- compileOnlyAarAsJar("androidx.compose.runtime:runtime:1.2.1")
- compileOnlyAarAsJar("androidx.compose.ui:ui:1.2.1")
- compileOnly(project(":lifecycle:lifecycle-common"))
- compileOnlyAarAsJar(project(":lifecycle:lifecycle-runtime"))
- compileOnlyAarAsJar("androidx.savedstate:savedstate:1.2.0")
-
- ksp(libs.moshiCodeGen)
-
- testImplementation(libs.assertj)
- testImplementationAarAsJar("androidx.compose.runtime:runtime:1.2.1")
-}
-
-androidx {
- name = "Paparazzi - AndroidX Fork"
- type = LibraryType.INTERNAL_HOST_TEST_LIBRARY
-}
\ No newline at end of file
diff --git a/external/paparazzi/paparazzi/lint-baseline.xml b/external/paparazzi/paparazzi/lint-baseline.xml
deleted file mode 100644
index a53cf10..0000000
--- a/external/paparazzi/paparazzi/lint-baseline.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.0.0-beta03" type="baseline" client="gradle" dependencies="false" name="AGP (8.0.0-beta03)" variant="all" version="8.0.0-beta03">
-
- <issue
- id="BanThreadSleep"
- message="Uses Thread.sleep()"
- errorLine1=" Thread.sleep(100)"
- errorLine2=" ~~~~~">
- <location
- file="src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt"/>
- </issue>
-
-</issues>
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/DeviceConfig.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/DeviceConfig.kt
deleted file mode 100644
index 7345548..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/DeviceConfig.kt
+++ /dev/null
@@ -1,541 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package app.cash.paparazzi
-
-import com.android.ide.common.rendering.api.HardwareConfig
-import com.android.ide.common.resources.configuration.CountryCodeQualifier
-import com.android.ide.common.resources.configuration.DensityQualifier
-import com.android.ide.common.resources.configuration.FolderConfiguration
-import com.android.ide.common.resources.configuration.KeyboardStateQualifier
-import com.android.ide.common.resources.configuration.LayoutDirectionQualifier
-import com.android.ide.common.resources.configuration.LocaleQualifier
-import com.android.ide.common.resources.configuration.NavigationMethodQualifier
-import com.android.ide.common.resources.configuration.NetworkCodeQualifier
-import com.android.ide.common.resources.configuration.NightModeQualifier
-import com.android.ide.common.resources.configuration.ScreenDimensionQualifier
-import com.android.ide.common.resources.configuration.ScreenOrientationQualifier
-import com.android.ide.common.resources.configuration.ScreenRatioQualifier
-import com.android.ide.common.resources.configuration.ScreenSizeQualifier
-import com.android.ide.common.resources.configuration.TextInputMethodQualifier
-import com.android.ide.common.resources.configuration.TouchScreenQualifier
-import com.android.ide.common.resources.configuration.UiModeQualifier
-import com.android.ide.common.resources.configuration.VersionQualifier
-import com.android.resources.Density
-import com.android.resources.Keyboard
-import com.android.resources.KeyboardState
-import com.android.resources.LayoutDirection
-import com.android.resources.Navigation
-import com.android.resources.NightMode
-import com.android.resources.NightMode.NOTNIGHT
-import com.android.resources.ScreenOrientation
-import com.android.resources.ScreenRatio
-import com.android.resources.ScreenSize
-import com.android.resources.TouchScreen
-import com.android.resources.UiMode
-import com.google.android.collect.Maps
-import java.io.File
-import java.io.FileInputStream
-import java.io.IOException
-import java.util.Properties
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
-import org.xmlpull.v1.XmlPullParserFactory
-
-/**
- * Provides [FolderConfiguration] and [HardwareConfig] for various devices. Also provides utility
- * methods to parse `build.prop` and `attrs.xml` to generate the appropriate maps.
- *
- * Defaults are for a Nexus 4 device.
- */
-data class DeviceConfig(
- val screenHeight: Int = 1280,
- val screenWidth: Int = 768,
- val xdpi: Int = 320,
- val ydpi: Int = 320,
- val orientation: ScreenOrientation = ScreenOrientation.PORTRAIT,
- val nightMode: NightMode = NOTNIGHT,
- val density: Density = Density.XHIGH,
- val fontScale: Float = 1f,
- val layoutDirection: LayoutDirection = LayoutDirection.LTR,
- val locale: String = "en",
- val ratio: ScreenRatio = ScreenRatio.NOTLONG,
- val size: ScreenSize = ScreenSize.NORMAL,
- val keyboard: Keyboard = Keyboard.NOKEY,
- val touchScreen: TouchScreen = TouchScreen.FINGER,
- val keyboardState: KeyboardState = KeyboardState.SOFT,
- val softButtons: Boolean = true,
- val navigation: Navigation = Navigation.NONAV,
- val released: String = "November 13, 2012"
-) {
- val folderConfiguration: FolderConfiguration
- get() = FolderConfiguration.createDefault()
- .apply {
- densityQualifier = DensityQualifier(density)
- navigationMethodQualifier = NavigationMethodQualifier(navigation)
- screenDimensionQualifier = when {
- screenWidth > screenHeight -> ScreenDimensionQualifier(screenWidth, screenHeight)
- else -> ScreenDimensionQualifier(screenHeight, screenWidth)
- }
- screenRatioQualifier = ScreenRatioQualifier(ratio)
- screenSizeQualifier = ScreenSizeQualifier(size)
- textInputMethodQualifier = TextInputMethodQualifier(keyboard)
- touchTypeQualifier = TouchScreenQualifier(touchScreen)
- keyboardStateQualifier = KeyboardStateQualifier(keyboardState)
- screenOrientationQualifier = ScreenOrientationQualifier(orientation)
-
- updateScreenWidthAndHeight()
- uiModeQualifier = UiModeQualifier(UiMode.NORMAL)
- nightModeQualifier = NightModeQualifier(nightMode)
- countryCodeQualifier = CountryCodeQualifier()
- layoutDirectionQualifier = LayoutDirectionQualifier(layoutDirection)
- networkCodeQualifier = NetworkCodeQualifier()
- localeQualifier = LocaleQualifier.getQualifier(locale)
- versionQualifier = VersionQualifier()
- }
-
- val hardwareConfig: HardwareConfig
- get() = HardwareConfig(
- screenWidth, screenHeight, density, xdpi.toFloat(), ydpi.toFloat(), size,
- orientation, null, softButtons
- )
-
- /**
- * Device specs per:
- * https://android.googlesource.com/platform/tools/base/+/mirror-goog-studio-master-dev/sdklib/src/main/java/com/android/sdklib/devices/nexus.xml
- *
- * Release dates obtained from Wikipedia.
- */
-
- companion object {
- @JvmField
- val NEXUS_4 = DeviceConfig()
-
- @JvmField
- val NEXUS_5 = DeviceConfig(
- screenHeight = 1920,
- screenWidth = 1080,
- xdpi = 445,
- ydpi = 445,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.XXHIGH,
- ratio = ScreenRatio.NOTLONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 31, 2013"
- )
-
- @JvmField
- val NEXUS_7 = DeviceConfig(
- screenHeight = 1920,
- screenWidth = 1200,
- xdpi = 323,
- ydpi = 323,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.XHIGH,
- ratio = ScreenRatio.NOTLONG,
- size = ScreenSize.LARGE,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "July 26, 2013"
- )
-
- @JvmField
- val NEXUS_10 = DeviceConfig(
- screenHeight = 1600,
- screenWidth = 2560,
- xdpi = 300,
- ydpi = 300,
- orientation = ScreenOrientation.LANDSCAPE,
- density = Density.XHIGH,
- ratio = ScreenRatio.NOTLONG,
- size = ScreenSize.XLARGE,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "November 13, 2012"
- )
-
- @JvmField
- val NEXUS_5_LAND = DeviceConfig(
- screenHeight = 1080,
- screenWidth = 1920,
- xdpi = 445,
- ydpi = 445,
- orientation = ScreenOrientation.LANDSCAPE,
- density = Density.XXHIGH,
- ratio = ScreenRatio.NOTLONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 31, 2013"
- )
-
- @JvmField
- val NEXUS_7_2012 = DeviceConfig(
- screenHeight = 1280,
- screenWidth = 800,
- xdpi = 195,
- ydpi = 200,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.TV,
- ratio = ScreenRatio.NOTLONG,
- size = ScreenSize.LARGE,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "July 13, 2012"
- )
-
- @JvmField
- val PIXEL_C = DeviceConfig(
- screenHeight = 1800,
- screenWidth = 2560,
- xdpi = 308,
- ydpi = 308,
- orientation = ScreenOrientation.LANDSCAPE,
- density = Density.XHIGH,
- ratio = ScreenRatio.NOTLONG,
- size = ScreenSize.XLARGE,
- keyboard = Keyboard.QWERTY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "December 8, 2015"
- )
-
- @JvmField
- val PIXEL = DeviceConfig(
- screenHeight = 1920,
- screenWidth = 1080,
- xdpi = 440,
- ydpi = 440,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_420,
- ratio = ScreenRatio.NOTLONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 20, 2016"
- )
-
- @JvmField
- val PIXEL_XL = DeviceConfig(
- screenHeight = 2560,
- screenWidth = 1440,
- xdpi = 534,
- ydpi = 534,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_560,
- ratio = ScreenRatio.NOTLONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 20, 2016"
- )
-
- @JvmField
- val PIXEL_2 = DeviceConfig(
- screenHeight = 1920,
- screenWidth = 1080,
- xdpi = 442,
- ydpi = 443,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_420,
- ratio = ScreenRatio.NOTLONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 19, 2017"
- )
-
- @JvmField
- val PIXEL_2_XL = DeviceConfig(
- screenHeight = 2880,
- screenWidth = 1440,
- xdpi = 537,
- ydpi = 537,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_560,
- ratio = ScreenRatio.LONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 19, 2017"
- )
-
- @JvmField
- val PIXEL_3 = DeviceConfig(
- screenHeight = 2160,
- screenWidth = 1080,
- xdpi = 442,
- ydpi = 442,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_440,
- ratio = ScreenRatio.LONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 18, 2018"
- )
-
- @JvmField
- val PIXEL_3_XL = DeviceConfig(
- screenHeight = 2960,
- screenWidth = 1440,
- xdpi = 522,
- ydpi = 522,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_560,
- ratio = ScreenRatio.LONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 18, 2018"
- )
-
- @JvmField
- val PIXEL_3A = DeviceConfig(
- screenHeight = 2220,
- screenWidth = 1080,
- xdpi = 442,
- ydpi = 444,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_440,
- ratio = ScreenRatio.LONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "May 7, 2019"
- )
-
- @JvmField
- val PIXEL_3A_XL = DeviceConfig(
- screenHeight = 2160,
- screenWidth = 1080,
- xdpi = 397,
- ydpi = 400,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_400,
- ratio = ScreenRatio.LONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "May 7, 2019"
- )
-
- @JvmField
- val PIXEL_4 = DeviceConfig(
- screenHeight = 2280,
- screenWidth = 1080,
- xdpi = 444,
- ydpi = 444,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_440,
- ratio = ScreenRatio.LONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 24, 2019"
- )
-
- @JvmField
- val PIXEL_4_XL = DeviceConfig(
- screenHeight = 3040,
- screenWidth = 1440,
- xdpi = 537,
- ydpi = 537,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_560,
- ratio = ScreenRatio.LONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 24, 2019"
- )
-
- @JvmField
- val PIXEL_4A = DeviceConfig(
- screenHeight = 2340,
- screenWidth = 1080,
- xdpi = 442,
- ydpi = 444,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_440,
- ratio = ScreenRatio.LONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "August 20, 2020"
- )
-
- @JvmField
- val PIXEL_5 = DeviceConfig(
- screenHeight = 2340,
- screenWidth = 1080,
- xdpi = 442,
- ydpi = 444,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_440,
- ratio = ScreenRatio.LONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 15, 2020"
- )
-
- @JvmField
- val PIXEL_6 = DeviceConfig(
- screenHeight = 2400,
- screenWidth = 1080,
- xdpi = 406,
- ydpi = 411,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_420,
- ratio = ScreenRatio.LONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 28, 2021"
- )
-
- @JvmField
- val PIXEL_6_PRO = DeviceConfig(
- screenHeight = 3120,
- screenWidth = 1440,
- xdpi = 512,
- ydpi = 512,
- orientation = ScreenOrientation.PORTRAIT,
- density = Density.DPI_560,
- ratio = ScreenRatio.LONG,
- size = ScreenSize.NORMAL,
- keyboard = Keyboard.NOKEY,
- touchScreen = TouchScreen.FINGER,
- keyboardState = KeyboardState.SOFT,
- softButtons = true,
- navigation = Navigation.NONAV,
- released = "October 28, 2021"
- )
-
- private const val TAG_ATTR = "attr"
- private const val TAG_ENUM = "enum"
- private const val TAG_FLAG = "flag"
- private const val ATTR_NAME = "name"
- private const val ATTR_VALUE = "value"
-
- @Throws(IOException::class)
- fun loadProperties(path: File): Map<String, String> {
- val p = Properties()
- val map = Maps.newHashMap<String, String>()
- p.load(FileInputStream(path))
- for (key in p.stringPropertyNames()) {
- map[key] = p.getProperty(key)
- }
- return map
- }
-
- @Throws(IOException::class, XmlPullParserException::class)
- fun getEnumMap(path: File): Map<String, Map<String, Int>> {
- val map = mutableMapOf<String, MutableMap<String, Int>>()
-
- val xmlPullParser = XmlPullParserFactory.newInstance()
- .newPullParser()
- xmlPullParser.setInput(FileInputStream(path), null)
- var eventType = xmlPullParser.eventType
- var attr: String? = null
- while (eventType != XmlPullParser.END_DOCUMENT) {
- if (eventType == XmlPullParser.START_TAG) {
- if (TAG_ATTR == xmlPullParser.name) {
- attr = xmlPullParser.getAttributeValue(null, ATTR_NAME)
- } else if (TAG_ENUM == xmlPullParser.name || TAG_FLAG == xmlPullParser.name) {
- val name = xmlPullParser.getAttributeValue(null, ATTR_NAME)
- val value = xmlPullParser.getAttributeValue(null, ATTR_VALUE)
- // Integer.decode cannot handle "ffffffff", see JDK issue 6624867
- val i = (java.lang.Long.decode(value) as Long).toInt()
- require(attr != null)
- var attributeMap: MutableMap<String, Int>? = map[attr]
- if (attributeMap == null) {
- attributeMap = Maps.newHashMap()
- map[attr] = attributeMap
- }
- attributeMap!![name] = i
- }
- } else if (eventType == XmlPullParser.END_TAG) {
- if (TAG_ATTR == xmlPullParser.name) {
- attr = null
- }
- }
- eventType = xmlPullParser.next()
- }
-
- return map
- }
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Environment.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Environment.kt
deleted file mode 100644
index 8cfae19..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Environment.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2019 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi
-
-import java.io.File
-import java.io.FileNotFoundException
-import java.nio.file.Path
-import java.nio.file.Paths
-import java.util.Locale
-import kotlin.io.path.exists
-
-data class Environment(
- val platformDir: String,
- val appTestDir: String,
- val resDir: String,
- val assetsDir: String,
- val compileSdkVersion: Int,
- val resourcePackageNames: List<String>
-) {
- init {
- val platformDirPath = Path.of(platformDir)
- if (!platformDirPath.exists()) {
- val elements = platformDirPath.nameCount
- val platform = platformDirPath.subpath(elements - 1, elements)
- val platformVersion = platform.toString().split("-").last()
- throw FileNotFoundException(
- "Missing platform version $platformVersion. " +
- "Install with sdkmanager --install \"platforms;$platform\""
- )
- }
- }
-}
-
-@Suppress("unused")
-fun androidHome() = System.getenv("ANDROID_SDK_ROOT")
- ?: System.getenv("ANDROID_HOME")
- ?: androidSdkPath()
-
-fun detectEnvironment(): Environment {
- checkInstalledJvm()
-
- val resourcesFile = File(System.getProperty("paparazzi.test.resources"))
- val configLines = resourcesFile.readLines()
-
- val appTestDir = Paths.get(System.getProperty("user.dir"))
- val androidHome = Paths.get(androidHome())
- return Environment(
- platformDir = androidHome.resolve(configLines[3]).toString(),
- appTestDir = appTestDir.toString(),
- resDir = appTestDir.resolve(configLines[1]).toString(),
- assetsDir = appTestDir.resolve(configLines[4]).toString(),
- compileSdkVersion = configLines[2].toInt(),
- resourcePackageNames = configLines[5].split(",")
- )
-}
-
-private fun androidSdkPath(): String {
- val osName = System.getProperty("os.name").lowercase(Locale.US)
- val sdkPathDir = if (osName.startsWith("windows")) {
- "\\AppData\\Local\\Android\\Sdk"
- } else if (osName.startsWith("mac")) {
- "/Library/Android/sdk"
- } else {
- "/Android/Sdk"
- }
- val homeDir = System.getProperty("user.home")
- return homeDir + sdkPathDir
-}
-
-private fun checkInstalledJvm() {
- val feature = try {
- // Runtime#version() only available as of Java 9.
- val version = Runtime::class.java.getMethod("version").invoke(null)
- // Runtime.Version#feature() only available as of Java 10.
- version.javaClass.getMethod("feature").invoke(version) as Int
- } catch (e: NoSuchMethodException) {
- -1
- }
-
- if (feature < 11) {
- throw IllegalStateException(
- "Unsupported JRE detected! Please install and run Paparazzi test suites on JDK 11+."
- )
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Flags.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Flags.kt
deleted file mode 100644
index 62c0b62..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Flags.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package app.cash.paparazzi
-
-object Flags {
- const val DEBUG_LINKED_OBJECTS = "app.cash.paparazzi.debug.linked.objects"
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt
deleted file mode 100644
index c43f4fb..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/HtmlReportWriter.kt
+++ /dev/null
@@ -1,294 +0,0 @@
-/*
- * Copyright (C) 2019 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi
-
-import app.cash.paparazzi.SnapshotHandler.FrameHandler
-import app.cash.paparazzi.internal.PaparazziJson
-import com.google.common.base.CharMatcher
-import java.awt.image.BufferedImage
-import java.io.File
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
-import java.util.UUID
-import javax.imageio.ImageIO
-import okio.BufferedSink
-import okio.HashingSink
-import okio.blackholeSink
-import okio.buffer
-import okio.sink
-import okio.source
-import org.jcodec.api.awt.AWTSequenceEncoder
-
-/**
- * Creates an HTML report that avoids writing files that have already been written.
- *
- * Images and videos are named by hashes of their contents. Paparazzi won't write two images or videos with the same
- * contents. Note that the images/ directory includes the individual frames of each video.
- *
- * Runs are named by their date.
- *
- * ```
- * images
- * 088c60580f06efa95c37fd8e754074729ee74a06.png
- * 93f9a81cb594280f4b3898d90dfad8c8ea969b01.png
- * 22d37abd0841ba2a8d0bd635954baf7cbfaa269b.png
- * a4769e43cc5901ef28c0d46c46a44ea6429cbccc.png
- * videos
- * d1cddc5da2224053f2af51f4e69a76de4e61fc41.mov
- * runs
- * 20190626002322_b9854e.js
- * 20190626002345_b1e882.js
- * index.html
- * index.js
- * paparazzi.js
- * ```
- */
-class HtmlReportWriter @JvmOverloads constructor(
- private val runName: String = defaultRunName(),
- private val rootDirectory: File = File("build/reports/paparazzi"),
- snapshotRootDirectory: File = File("src/test/snapshots")
-) : SnapshotHandler {
- private val runsDirectory: File = File(rootDirectory, "runs")
- private val imagesDirectory: File = File(rootDirectory, "images")
- private val videosDirectory: File = File(rootDirectory, "videos")
-
- private val goldenImagesDirectory = File(snapshotRootDirectory, "images")
- private val goldenVideosDirectory = File(snapshotRootDirectory, "videos")
-
- private val shots = mutableListOf<Snapshot>()
-
- private val isRecording: Boolean =
- System.getProperty("paparazzi.test.record")?.toBoolean() == true
-
- init {
- runsDirectory.mkdirs()
- imagesDirectory.mkdirs()
- videosDirectory.mkdirs()
- writeStaticFiles()
- writeRunJs()
- writeIndexJs()
- }
-
- override fun newFrameHandler(
- snapshot: Snapshot,
- frameCount: Int,
- fps: Int
- ): FrameHandler {
- return object : FrameHandler {
- val hashes = mutableListOf<String>()
-
- override fun handle(image: BufferedImage) {
- hashes += writeImage(image)
- }
-
- override fun close() {
- if (hashes.isEmpty()) return
-
- val shot = if (hashes.size == 1) {
- val original = File(imagesDirectory, "${hashes[0]}.png")
- if (isRecording) {
- val goldenFile = File(goldenImagesDirectory, snapshot.toFileName("_", "png"))
- original.copyTo(goldenFile, overwrite = true)
- }
- snapshot.copy(file = original.toJsonPath())
- } else {
- val hash = writeVideo(hashes, fps)
-
- if (isRecording) {
- for ((index, frameHash) in hashes.withIndex()) {
- val originalFrame = File(imagesDirectory, "$frameHash.png")
- val frameSnapshot = snapshot.copy(name = "${snapshot.name} $index")
- val goldenFile = File(goldenImagesDirectory, frameSnapshot.toFileName("_", "png"))
- if (!goldenFile.exists()) {
- originalFrame.copyTo(goldenFile)
- }
- }
- }
- val original = File(videosDirectory, "$hash.mov")
- if (isRecording) {
- val goldenFile = File(goldenVideosDirectory, snapshot.toFileName("_", "mov"))
- if (!goldenFile.exists()) {
- original.copyTo(goldenFile)
- }
- }
- snapshot.copy(file = original.toJsonPath())
- }
-
- shots += shot
- }
- }
- }
-
- /** Returns the hash of the image. */
- private fun writeImage(image: BufferedImage): String {
- val hash = hash(image)
- val file = File(imagesDirectory, "$hash.png")
- if (!file.exists()) {
- file.writeAtomically(image)
- }
- return hash
- }
-
- /** Returns a SHA-1 hash of the pixels of [image]. */
- private fun hash(image: BufferedImage): String {
- val hashingSink = HashingSink.sha1(blackholeSink())
- hashingSink.buffer().use { sink ->
- for (y in 0 until image.height) {
- for (x in 0 until image.width) {
- sink.writeInt(image.getRGB(x, y))
- }
- }
- }
- return hashingSink.hash.hex()
- }
-
- private fun writeVideo(
- frameHashes: List<String>,
- fps: Int
- ): String {
- val hash = hash(frameHashes)
- val file = File(videosDirectory, "$hash.mov")
- if (!file.exists()) {
- val tmpFile = File(videosDirectory, "$hash.mov.tmp")
- val encoder = AWTSequenceEncoder.createSequenceEncoder(tmpFile, fps)
- for (frameHash in frameHashes) {
- val frame = ImageIO.read(File(imagesDirectory, "$frameHash.png"))
- encoder.encodeImage(frame)
- }
- encoder.finish()
- tmpFile.renameTo(file)
- }
- return hash
- }
-
- /** Returns a SHA-1 hash of [lines]. */
- private fun hash(lines: List<String>): String {
- val hashingSink = HashingSink.sha1(blackholeSink())
- hashingSink.buffer().use { sink ->
- for (hash in lines) {
- sink.writeUtf8(hash)
- sink.writeUtf8("\n")
- }
- }
- return hashingSink.hash.hex()
- }
-
- /** Release all resources and block until everything has been written to the file system. */
- override fun close() {
- writeRunJs()
- }
-
- /**
- * Emits the all runs index, which reads like JSON with an executable header.
- *
- * ```
- * window.all_runs = [
- * "20190319153912aaab",
- * "20190319153917bcfe"
- * ];
- * ```
- */
- private fun writeIndexJs() {
- val runNames = mutableListOf<String>()
- val runs = runsDirectory.list().sorted()
- for (run in runs) {
- if (run.endsWith(".js")) {
- runNames += run.substring(0, run.length - 3)
- }
- }
-
- File(rootDirectory, "index.js").writeAtomically {
- writeUtf8("window.all_runs = ")
- PaparazziJson.listOfStringsAdapter.toJson(this, runNames)
- writeUtf8(";")
- }
- }
-
- /**
- * Emits a run index, which reads like JSON with an executable header.
- *
- * ```
- * window.runs["20190319153912aaab"] = [
- * {
- * "name": "loading",
- * "testName": "app.cash.CelebrityTest#testSettings",
- * "timestamp": "2019-03-20T10:27:43Z",
- * "tags": ["redesign"],
- * "file": "loading.png"
- * },
- * {
- * "name": "error",
- * "testName": "app.cash.CelebrityTest#testSettings",
- * "timestamp": "2019-03-20T10:27:43Z",
- * "tags": ["redesign"],
- * "file": "error.png"
- * }
- * ];
- * ```
- */
- private fun writeRunJs() {
- val runJs = File(runsDirectory, "${runName.sanitizeForFilename()}.js")
- runJs.writeAtomically {
- writeUtf8("window.runs[\"$runName\"] = ")
- PaparazziJson.listOfShotsAdapter.toJson(this, shots)
- writeUtf8(";")
- }
- }
-
- private fun writeStaticFiles() {
- for (staticFile in listOf("index.html", "paparazzi.js")) {
- File(rootDirectory, staticFile).writeAtomically {
- writeAll(HtmlReportWriter::class.java.classLoader.getResourceAsStream(staticFile).source())
- }
- }
- }
-
- private fun File.writeAtomically(bufferedImage: BufferedImage) {
- val tmpFile = File(parentFile, "$name.tmp")
- ImageIO.write(bufferedImage, "PNG", tmpFile)
- delete()
- tmpFile.renameTo(this)
- }
-
- private fun File.writeAtomically(writerAction: BufferedSink.() -> Unit) {
- val tmpFile = File(parentFile, "$name.tmp")
- tmpFile.sink()
- .buffer()
- .use { sink ->
- sink.writerAction()
- }
- delete()
- tmpFile.renameTo(this)
- }
-
- private fun File.toJsonPath(): String = relativeTo(rootDirectory).invariantSeparatorsPath
-}
-
-internal fun defaultRunName(): String {
- val now = Date()
- val timestamp = SimpleDateFormat("yyyyMMddHHmmss", Locale.US).format(now)
- val token = UUID.randomUUID().toString().substring(0, 6)
- return "${timestamp}_$token"
-}
-
-internal val filenameSafeChars = CharMatcher.inRange('a', 'z')
- .or(CharMatcher.inRange('0', '9'))
- .or(CharMatcher.anyOf("_-.~@^()[]{}:;,"))
-
-internal fun String.sanitizeForFilename(): String? {
- return filenameSafeChars.negate().replaceFrom(lowercase(Locale.US), '_')
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt
deleted file mode 100644
index 2c9dec41..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt
+++ /dev/null
@@ -1,610 +0,0 @@
-/*
- * Copyright (C) 2019 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi
-
-import android.animation.AnimationHandler
-import android.content.Context
-import android.content.res.Resources
-import android.graphics.Bitmap
-import android.os.Handler_Delegate
-import android.os.SystemClock_Delegate
-import android.util.AttributeSet
-import android.util.DisplayMetrics
-import android.view.BridgeInflater
-import android.view.Choreographer
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewGroup.LayoutParams
-import android.widget.FrameLayout
-import androidx.annotation.LayoutRes
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.ComposeView
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LifecycleRegistry
-import androidx.lifecycle.setViewTreeLifecycleOwner
-import androidx.savedstate.SavedStateRegistry
-import androidx.savedstate.SavedStateRegistryController
-import androidx.savedstate.SavedStateRegistryOwner
-import androidx.savedstate.setViewTreeSavedStateRegistryOwner
-import app.cash.paparazzi.agent.AgentTestRule
-import app.cash.paparazzi.agent.InterceptorRegistrar
-import app.cash.paparazzi.internal.ChoreographerDelegateInterceptor
-import app.cash.paparazzi.internal.EditModeInterceptor
-import app.cash.paparazzi.internal.IInputMethodManagerInterceptor
-import app.cash.paparazzi.internal.ImageUtils
-import app.cash.paparazzi.internal.MatrixMatrixMultiplicationInterceptor
-import app.cash.paparazzi.internal.MatrixVectorMultiplicationInterceptor
-import app.cash.paparazzi.internal.PaparazziCallback
-import app.cash.paparazzi.internal.PaparazziLogger
-import app.cash.paparazzi.internal.Renderer
-import app.cash.paparazzi.internal.ResourcesInterceptor
-import app.cash.paparazzi.internal.ServiceManagerInterceptor
-import app.cash.paparazzi.internal.SessionParamsBuilder
-import app.cash.paparazzi.internal.parsers.LayoutPullParser
-import com.android.ide.common.rendering.api.RenderSession
-import com.android.ide.common.rendering.api.Result
-import com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN
-import com.android.ide.common.rendering.api.SessionParams
-import com.android.ide.common.rendering.api.SessionParams.RenderingMode
-import com.android.internal.lang.System_Delegate
-import com.android.layoutlib.bridge.Bridge
-import com.android.layoutlib.bridge.Bridge.cleanupThread
-import com.android.layoutlib.bridge.Bridge.prepareThread
-import com.android.layoutlib.bridge.BridgeRenderSession
-import com.android.layoutlib.bridge.impl.RenderAction
-import com.android.layoutlib.bridge.impl.RenderSessionImpl
-import java.awt.image.BufferedImage
-import java.util.Date
-import java.util.concurrent.TimeUnit
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
-
-class Paparazzi @JvmOverloads constructor(
- private val environment: Environment = detectEnvironment(),
- private val deviceConfig: DeviceConfig = DeviceConfig.NEXUS_5,
- private val theme: String = "android:Theme.Material.NoActionBar.Fullscreen",
- private val renderingMode: RenderingMode = RenderingMode.NORMAL,
- private val appCompatEnabled: Boolean = true,
- private val maxPercentDifference: Double = 0.1,
- private val snapshotHandler: SnapshotHandler = determineHandler(maxPercentDifference),
- private val renderExtensions: Set<RenderExtension> = setOf()
-) : TestRule {
- private val logger = PaparazziLogger()
- private lateinit var renderSession: RenderSessionImpl
- private lateinit var bridgeRenderSession: RenderSession
- private var testName: TestName? = null
-
- val layoutInflater: LayoutInflater
- get() = RenderAction.getCurrentContext().getSystemService("layout_inflater") as BridgeInflater
-
- val resources: Resources
- get() = RenderAction.getCurrentContext().resources
-
- val context: Context
- get() = RenderAction.getCurrentContext()
-
- /**
- * The root layout that test views will be placed into. The FrameLayout is dynamically set to
- * `wrap_content` if the `renderMode` is `RenderingMode.SizeAction.SHRINK` in the appropriate
- * direction, otherwise it is set to `match_parent`.
- */
- private val contentRoot = """
- |<?xml version="1.0" encoding="utf-8"?>
- |<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- | android:layout_width="${renderingMode.horizAction.toAttrValue()}"
- | android:layout_height="${renderingMode.vertAction.toAttrValue()}"/>
- """.trimMargin()
-
- private fun RenderingMode.SizeAction.toAttrValue() =
- if (this == RenderingMode.SizeAction.SHRINK) "wrap_content" else "match_parent"
-
- override fun apply(
- base: Statement,
- description: Description
- ): Statement {
- val statement = object : Statement() {
- override fun evaluate() {
- prepare(description)
- try {
- base.evaluate()
- } finally {
- close()
- logger.assertNoErrors()
- }
- }
- }
-
- return if (!isInitialized) {
- registerFontLookupInterceptionIfResourceCompatDetected()
- registerViewEditModeInterception()
- registerMatrixMultiplyInterception()
- registerChoreographerDelegateInterception()
- registerServiceManagerInterception()
- registerIInputMethodManagerInterception()
-
- val outerRule = AgentTestRule()
- outerRule.apply(statement, description)
- } else {
- statement
- }
- }
-
- fun prepare(description: Description) {
- forcePlatformSdkVersion(environment.compileSdkVersion)
-
- val layoutlibCallback = PaparazziCallback(logger, environment.resourcePackageNames)
- layoutlibCallback.initResources()
-
- testName = description.toTestName()
-
- if (!isInitialized) {
- renderer = Renderer(environment, layoutlibCallback, logger, maxPercentDifference)
- sessionParamsBuilder = renderer.prepare()
- }
-
- sessionParamsBuilder = sessionParamsBuilder
- .copy(
- layoutPullParser = LayoutPullParser.createFromString(contentRoot),
- deviceConfig = deviceConfig,
- renderingMode = renderingMode
- )
- .withTheme(theme)
-
- val sessionParams = sessionParamsBuilder.build()
- renderSession = createRenderSession(sessionParams)
- prepareThread()
- renderSession.init(sessionParams.timeout)
- Bitmap.setDefaultDensity(DisplayMetrics.DENSITY_DEVICE_STABLE)
-
- // requires LayoutInflater to be created, which is a side-effect of RenderSessionImpl.init()
- if (appCompatEnabled) {
- initializeAppCompatIfPresent()
- }
-
- bridgeRenderSession = createBridgeSession(renderSession, renderSession.inflate())
- }
-
- fun close() {
- testName = null
- renderSession.release()
- bridgeRenderSession.dispose()
- cleanupThread()
- snapshotHandler.close()
-
- renderer.dumpDelegates()
- }
-
- @Suppress("UNCHECKED_CAST")
- fun <V : View> inflate(@LayoutRes layoutId: Int): V =
- layoutInflater.inflate(layoutId, null) as V
-
- fun snapshot(name: String? = null, composable: @Composable () -> Unit) {
- val hostView = ComposeView(context)
- // During onAttachedToWindow, AbstractComposeView will attempt to resolve its parent's
- // CompositionContext, which requires first finding the "content view", then using that to
- // find a root view with a ViewTreeLifecycleOwner
- val parent = FrameLayout(context).apply { id = android.R.id.content }
- parent.addView(
- hostView,
- renderingMode.horizAction.toLayoutParams(),
- renderingMode.vertAction.toLayoutParams()
- )
- PaparazziComposeOwner.register(parent)
- hostView.setContent(composable)
-
- try {
- snapshot(parent, name)
- } finally {
- forceReleaseComposeReferenceLeaks()
- }
- }
-
- private fun RenderingMode.SizeAction.toLayoutParams() =
- if (this == RenderingMode.SizeAction.SHRINK) {
- LayoutParams.WRAP_CONTENT
- } else {
- LayoutParams.MATCH_PARENT
- }
-
- @JvmOverloads
- fun snapshot(view: View, name: String? = null) {
- takeSnapshots(view, name, 0, -1, 1)
- }
-
- @JvmOverloads
- fun gif(
- view: View,
- name: String? = null,
- start: Long = 0L,
- end: Long = 500L,
- fps: Int = 30
- ) {
- // Add one to the frame count so we get the last frame. Otherwise a 1 second, 60 FPS animation
- // our 60th frame will be at time 983 ms, and we want our last frame to be 1,000 ms. This gets
- // us 61 frames for a 1 second animation, 121 frames for a 2 second animation, etc.
- val durationMillis = (end - start).toInt()
- val frameCount = (durationMillis * fps) / 1000 + 1
- val startNanos = TimeUnit.MILLISECONDS.toNanos(start)
- takeSnapshots(view, name, startNanos, fps, frameCount)
- }
-
- fun unsafeUpdateConfig(
- deviceConfig: DeviceConfig? = null,
- theme: String? = null,
- renderingMode: RenderingMode? = null
- ) {
- require(deviceConfig != null || theme != null || renderingMode != null) {
- "Calling unsafeUpdateConfig requires at least one non-null argument."
- }
-
- renderSession.release()
- bridgeRenderSession.dispose()
- cleanupThread()
-
- sessionParamsBuilder = sessionParamsBuilder
- .copy(
- // Required to reset underlying parser stream
- layoutPullParser = LayoutPullParser.createFromString(contentRoot)
- )
-
- if (deviceConfig != null) {
- sessionParamsBuilder = sessionParamsBuilder.copy(deviceConfig = deviceConfig)
- }
-
- if (theme != null) {
- sessionParamsBuilder = sessionParamsBuilder.withTheme(theme)
- }
-
- if (renderingMode != null) {
- sessionParamsBuilder = sessionParamsBuilder.copy(renderingMode = renderingMode)
- }
-
- val sessionParams = sessionParamsBuilder.build()
- renderSession = createRenderSession(sessionParams)
- prepareThread()
- renderSession.init(sessionParams.timeout)
- Bitmap.setDefaultDensity(DisplayMetrics.DENSITY_DEVICE_STABLE)
- bridgeRenderSession = createBridgeSession(renderSession, renderSession.inflate())
- }
-
- private fun takeSnapshots(
- view: View,
- name: String?,
- startNanos: Long,
- fps: Int,
- frameCount: Int
- ) {
- val snapshot = Snapshot(name, testName!!, Date())
-
- val frameHandler = snapshotHandler.newFrameHandler(snapshot, frameCount, fps)
- frameHandler.use {
- val viewGroup = bridgeRenderSession.rootViews[0].viewObject as ViewGroup
- val modifiedView = renderExtensions.fold(view) { view, renderExtension ->
- renderExtension.renderView(view)
- }
-
- System_Delegate.setBootTimeNanos(0L)
- try {
- withTime(0L) {
- // Initialize the choreographer at time=0.
- }
-
- viewGroup.addView(modifiedView)
- for (frame in 0 until frameCount) {
- val nowNanos = (startNanos + (frame * 1_000_000_000.0 / fps)).toLong()
- withTime(nowNanos) {
- val result = renderSession.render(true)
- if (result.status == ERROR_UNKNOWN) {
- throw result.exception
- }
-
- val image = bridgeRenderSession.image
- frameHandler.handle(scaleImage(image))
- }
- }
- } finally {
- viewGroup.removeView(modifiedView)
- AnimationHandler.sAnimatorHandler.set(null)
- }
- }
- }
-
- private fun withTime(
- timeNanos: Long,
- block: () -> Unit
- ) {
- val frameNanos = TIME_OFFSET_NANOS + timeNanos
-
- // Execute the block at the requested time.
- System_Delegate.setNanosTime(frameNanos)
-
- val choreographer = Choreographer.getInstance()
- val areCallbacksRunningField = choreographer::class.java.getDeclaredField("mCallbacksRunning")
- areCallbacksRunningField.isAccessible = true
-
- try {
- areCallbacksRunningField.setBoolean(choreographer, true)
-
- // https://android.googlesource.com/platform/frameworks/layoutlib/+/d58aa4703369e109b24419548f38b422d5a44738/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java#171
- // BridgeRenderSession.executeCallbacks aggressively tears down the main Looper and BridgeContext, so we call the static delegates ourselves.
- Handler_Delegate.executeCallbacks()
- val currentTimeMs = SystemClock_Delegate.uptimeMillis()
- val choreographerCallbacks =
- RenderAction.getCurrentContext().sessionInteractiveData.choreographerCallbacks
- choreographerCallbacks.execute(currentTimeMs, Bridge.getLog())
-
- block()
- } catch (e: Throwable) {
- Bridge.getLog().error("broken", "Failed executing Choreographer#doFrame", e, null, null)
- throw e
- } finally {
- areCallbacksRunningField.setBoolean(choreographer, false)
- }
- }
-
- private fun createRenderSession(sessionParams: SessionParams): RenderSessionImpl {
- val renderSession = RenderSessionImpl(sessionParams)
- renderSession.setElapsedFrameTimeNanos(0L)
- RenderSessionImpl::class.java
- .getDeclaredField("mFirstFrameExecuted")
- .apply {
- isAccessible = true
- set(renderSession, true)
- }
- return renderSession
- }
-
- private fun createBridgeSession(
- renderSession: RenderSessionImpl,
- result: Result
- ): BridgeRenderSession {
- try {
- val bridgeSessionClass = Class.forName("com.android.layoutlib.bridge.BridgeRenderSession")
- val constructor =
- bridgeSessionClass.getDeclaredConstructor(RenderSessionImpl::class.java, Result::class.java)
- constructor.isAccessible = true
- return constructor.newInstance(renderSession, result) as BridgeRenderSession
- } catch (e: Exception) {
- throw RuntimeException(e)
- }
- }
-
- private fun scaleImage(image: BufferedImage): BufferedImage {
- val scale = ImageUtils.getThumbnailScale(image)
- // Only scale images down so we don't waste storage space enlarging smaller layouts.
- return if (scale < 1f) ImageUtils.scale(image, scale, scale) else image
- }
-
- private fun Description.toTestName(): TestName {
- val fullQualifiedName = className
- val packageName = fullQualifiedName.substringBeforeLast('.', missingDelimiterValue = "")
- val className = fullQualifiedName.substringAfterLast('.')
- return TestName(packageName, className, methodName)
- }
-
- private fun forcePlatformSdkVersion(compileSdkVersion: Int) {
- val buildVersionClass = try {
- Paparazzi::class.java.classLoader.loadClass("android.os.Build\$VERSION")
- } catch (e: ClassNotFoundException) {
- // Project unit tests don't load Android platform code
- return
- }
- buildVersionClass
- .getFieldReflectively("SDK_INT")
- .setStaticValue(compileSdkVersion)
- }
-
- private fun initializeAppCompatIfPresent() {
- lateinit var appCompatDelegateClass: Class<*>
- try {
- // See androidx.appcompat.widget.AppCompatDrawableManager#preload()
- val appCompatDrawableManagerClass =
- Class.forName("androidx.appcompat.widget.AppCompatDrawableManager")
- val preloadMethod = appCompatDrawableManagerClass.getMethod("preload")
- preloadMethod.invoke(null)
-
- appCompatDelegateClass = Class.forName("androidx.appcompat.app.AppCompatDelegate")
- } catch (e: ClassNotFoundException) {
- logger.verbose("AppCompat not found on classpath")
- return
- }
-
- // See androidx.appcompat.app.AppCompatDelegateImpl#installViewFactory()
- if (layoutInflater.factory == null) {
- layoutInflater.factory2 = object : LayoutInflater.Factory2 {
- override fun onCreateView(
- parent: View?,
- name: String,
- context: Context,
- attrs: AttributeSet
- ): View? {
- val appCompatViewInflaterClass =
- Class.forName("androidx.appcompat.app.AppCompatViewInflater")
-
- val createViewMethod = appCompatViewInflaterClass
- .getDeclaredMethod(
- "createView",
- View::class.java,
- String::class.java,
- Context::class.java,
- AttributeSet::class.java,
- Boolean::class.javaPrimitiveType,
- Boolean::class.javaPrimitiveType,
- Boolean::class.javaPrimitiveType,
- Boolean::class.javaPrimitiveType
- )
- .apply { isAccessible = true }
-
- val inheritContext = true
- val readAndroidTheme = true
- val readAppTheme = true
- val wrapContext = true
-
- val newAppCompatViewInflaterInstance = appCompatViewInflaterClass
- .getConstructor()
- .newInstance()
-
- return createViewMethod.invoke(
- newAppCompatViewInflaterInstance, parent, name, context, attrs,
- inheritContext, readAndroidTheme, readAppTheme, wrapContext
- ) as View?
- }
-
- override fun onCreateView(
- name: String,
- context: Context,
- attrs: AttributeSet
- ): View? = onCreateView(null, name, context, attrs)
- }
- } else {
- if (!appCompatDelegateClass.isAssignableFrom(layoutInflater.factory2::class.java)) {
- throw IllegalStateException(
- "The LayoutInflater already has a Factory installed so we can not install AppCompat's"
- )
- }
- }
- }
-
- /**
- * Current workaround for supporting custom fonts when constructing views in code. This check
- * may be used or expanded to support other cases requiring similar method interception
- * techniques.
- *
- * See:
- * https://github.com/cashapp/paparazzi/issues/119
- * https://issuetracker.google.com/issues/156065472
- */
- private fun registerFontLookupInterceptionIfResourceCompatDetected() {
- try {
- val resourcesCompatClass = Class.forName("androidx.core.content.res.ResourcesCompat")
- InterceptorRegistrar.addMethodInterceptor(
- resourcesCompatClass,
- "getFont",
- ResourcesInterceptor::class.java
- )
- } catch (e: ClassNotFoundException) {
- logger.verbose("ResourceCompat not found on classpath")
- }
- }
-
- private fun registerServiceManagerInterception() {
- val serviceManager = Class.forName("android.os.ServiceManager")
- InterceptorRegistrar.addMethodInterceptor(
- serviceManager,
- "getServiceOrThrow",
- ServiceManagerInterceptor::class.java
- )
- }
-
- private fun registerIInputMethodManagerInterception() {
- val iimm = Class.forName("com.android.internal.view.IInputMethodManager\$Stub")
- InterceptorRegistrar.addMethodInterceptor(
- iimm,
- "asInterface",
- IInputMethodManagerInterceptor::class.java
- )
- }
-
- private fun registerViewEditModeInterception() {
- val viewClass = Class.forName("android.view.View")
- InterceptorRegistrar.addMethodInterceptor(
- viewClass,
- "isInEditMode",
- EditModeInterceptor::class.java
- )
- }
-
- private fun registerMatrixMultiplyInterception() {
- val matrixClass = Class.forName("android.opengl.Matrix")
- InterceptorRegistrar.addMethodInterceptors(
- matrixClass,
- setOf(
- "multiplyMM" to MatrixMatrixMultiplicationInterceptor::class.java,
- "multiplyMV" to MatrixVectorMultiplicationInterceptor::class.java
- )
- )
- }
-
- private fun registerChoreographerDelegateInterception() {
- val choreographerDelegateClass = Class.forName("android.view.Choreographer_Delegate")
- InterceptorRegistrar.addMethodInterceptor(
- choreographerDelegateClass,
- "getFrameTimeNanos",
- ChoreographerDelegateInterceptor::class.java
- )
- }
-
- private fun forceReleaseComposeReferenceLeaks() {
- // AndroidUiDispatcher is backed by a Handler, by executing one last time
- // we give the dispatcher the ability to clean-up / release its callbacks.
- executeHandlerCallbacks()
- }
-
- private fun executeHandlerCallbacks() {
- // Avoid ConcurrentModificationException in
- // RenderAction.currentContext.sessionInteractiveData.handlerMessageQueue.runnablesMap which is a WeakHashMap
- // https://android.googlesource.com/platform/tools/adt/idea/+/c331c9b2f4334748c55c29adec3ad1cd67e45df2/designer/src/com/android/tools/idea/uibuilder/scene/LayoutlibSceneManager.java#1558
- synchronized(this) {
- // https://android.googlesource.com/platform/frameworks/layoutlib/+/d58aa4703369e109b24419548f38b422d5a44738/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java#171
- // BridgeRenderSession.executeCallbacks aggressively tears down the main Looper and BridgeContext, so we call the static delegates ourselves.
- Handler_Delegate.executeCallbacks()
- }
- }
-
- private class PaparazziComposeOwner private constructor() :
- LifecycleOwner, SavedStateRegistryOwner {
- private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
- private val savedStateRegistryController = SavedStateRegistryController.create(this)
-
- override val lifecycle: Lifecycle
- get() = lifecycleRegistry
- override val savedStateRegistry: SavedStateRegistry =
- savedStateRegistryController.savedStateRegistry
-
- companion object {
- fun register(view: View) {
- val owner = PaparazziComposeOwner()
- owner.savedStateRegistryController.performRestore(null)
- owner.lifecycleRegistry.currentState = Lifecycle.State.CREATED
- view.setViewTreeLifecycleOwner(owner)
- view.setViewTreeSavedStateRegistryOwner(owner)
- }
- }
- }
-
- companion object {
- /** The choreographer doesn't like 0 as a frame time, so start an hour later. */
- internal val TIME_OFFSET_NANOS = TimeUnit.HOURS.toNanos(1L)
-
- internal lateinit var renderer: Renderer
- internal val isInitialized get() = ::renderer.isInitialized
-
- internal lateinit var sessionParamsBuilder: SessionParamsBuilder
-
- private val isVerifying: Boolean =
- System.getProperty("paparazzi.test.verify")?.toBoolean() == true
-
- private fun determineHandler(maxPercentDifference: Double): SnapshotHandler =
- if (isVerifying) {
- SnapshotVerifier(maxPercentDifference)
- } else {
- HtmlReportWriter()
- }
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Reflections.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Reflections.kt
deleted file mode 100644
index 15617f0..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Reflections.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package app.cash.paparazzi
-
-import java.lang.reflect.Field
-import java.lang.reflect.Modifier
-import java.security.PrivilegedAction
-import sun.misc.Unsafe
-
-/**
- * Inspired by and ported from:
- * https://github.com/powermock/powermock/commit/fc092c5d7e339d01e079184a2a0e88b5c46fc0e8
- * https://github.com/powermock/powermock/commit/bd92bcc5329c4981cf09dece5c3eafcf92fe49ff
- */
-internal fun Class<*>.getFieldReflectively(fieldName: String): Field =
- try {
- this.getDeclaredField(fieldName).also { it.isAccessible = true }
- } catch (e: NoSuchFieldException) {
- throw RuntimeException("Field '$fieldName' was not found in class $name.")
- }
-
-internal fun Field.setStaticValue(value: Any) {
- try {
- this.isAccessible = true
- val isFinalModifierPresent = this.modifiers and Modifier.FINAL == Modifier.FINAL
- if (isFinalModifierPresent) {
- @Suppress("DEPRECATION")
- java.security.AccessController.doPrivileged<Any?>(
- PrivilegedAction {
- try {
- val unsafe = Unsafe::class.java.getFieldReflectively("theUnsafe").get(null) as Unsafe
- val offset = unsafe.staticFieldOffset(this)
- val base = unsafe.staticFieldBase(this)
- unsafe.setFieldValue(this, base, offset, value)
- null
- } catch (t: Throwable) {
- throw RuntimeException(t)
- }
- }
- )
- } else {
- this.set(null, value)
- }
- } catch (ex: SecurityException) {
- throw RuntimeException(ex)
- } catch (ex: IllegalAccessException) {
- throw RuntimeException(ex)
- } catch (ex: IllegalArgumentException) {
- throw RuntimeException(ex)
- }
-}
-
-internal fun Unsafe.setFieldValue(field: Field, base: Any, offset: Long, value: Any) =
- when (field.type) {
- Integer.TYPE -> this.putInt(base, offset, (value as Int))
- java.lang.Short.TYPE -> this.putShort(base, offset, (value as Short))
- java.lang.Long.TYPE -> this.putLong(base, offset, (value as Long))
- java.lang.Byte.TYPE -> this.putByte(base, offset, (value as Byte))
- java.lang.Boolean.TYPE -> this.putBoolean(base, offset, (value as Boolean))
- java.lang.Float.TYPE -> this.putFloat(base, offset, (value as Float))
- java.lang.Double.TYPE -> this.putDouble(base, offset, (value as Double))
- Character.TYPE -> this.putChar(base, offset, (value as Char))
- else -> this.putObject(base, offset, value)
- }
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/RenderExtension.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/RenderExtension.kt
deleted file mode 100644
index d0d12b2..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/RenderExtension.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2021 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi
-
-import android.view.View
-
-/**
- * An extension for overlaying additional information on top of each rendered frame.
- */
-interface RenderExtension {
- /**
- * Allows this extension to modify the view hierarchy represented by [contentView].
- *
- * Returns the root view of the modified hierarchy.
- */
- fun renderView(contentView: View): View
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt
deleted file mode 100644
index 85501f7..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Snapshot.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2019 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi
-
-import com.squareup.moshi.JsonClass
-import java.util.Date
-import java.util.Locale
-
-@JsonClass(generateAdapter = true)
-data class Snapshot(
- val name: String?,
- val testName: TestName,
- val timestamp: Date,
- val tags: List<String> = listOf(),
- val file: String? = null
-)
-
-internal fun Snapshot.toFileName(
- delimiter: String = "_",
- extension: String
-): String {
- val formattedLabel = if (name != null) {
- "$delimiter${name.lowercase(Locale.US).replace("\\s".toRegex(), delimiter)}"
- } else {
- ""
- }
- return "${testName.packageName}${delimiter}${testName.className}" +
- "${delimiter}${testName.methodName}$formattedLabel.$extension"
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/SnapshotHandler.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/SnapshotHandler.kt
deleted file mode 100644
index 5067f25..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/SnapshotHandler.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2019 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi
-
-import java.awt.image.BufferedImage
-import java.io.Closeable
-
-interface SnapshotHandler : Closeable {
- fun newFrameHandler(
- snapshot: Snapshot,
- frameCount: Int,
- fps: Int
- ): FrameHandler
-
- interface FrameHandler : Closeable {
- fun handle(image: BufferedImage)
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/SnapshotVerifier.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/SnapshotVerifier.kt
deleted file mode 100644
index 564cd2e..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/SnapshotVerifier.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2020 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi
-
-import app.cash.paparazzi.SnapshotHandler.FrameHandler
-import app.cash.paparazzi.internal.ImageUtils
-import java.awt.image.BufferedImage
-import java.io.File
-import javax.imageio.ImageIO
-
-class SnapshotVerifier @JvmOverloads constructor(
- private val maxPercentDifference: Double,
- rootDirectory: File = File("src/test/snapshots")
-) : SnapshotHandler {
- private val imagesDirectory: File = File(rootDirectory, "images")
- private val videosDirectory: File = File(rootDirectory, "videos")
-
- init {
- imagesDirectory.mkdirs()
- videosDirectory.mkdirs()
- }
-
- override fun newFrameHandler(
- snapshot: Snapshot,
- frameCount: Int,
- fps: Int
- ): FrameHandler {
- return object : FrameHandler {
- override fun handle(image: BufferedImage) {
- // Note: does not handle videos or its frames at the moment
- val expected = File(imagesDirectory, snapshot.toFileName(extension = "png"))
- if (!expected.exists()) {
- throw AssertionError("File $expected does not exist")
- }
-
- val goldenImage = ImageIO.read(expected)
- ImageUtils.assertImageSimilar(
- relativePath = expected.path,
- image = image,
- goldenImage = goldenImage,
- maxPercentDifferent = maxPercentDifference
- )
- }
-
- override fun close() = Unit
- }
- }
-
- override fun close() = Unit
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/TestName.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/TestName.kt
deleted file mode 100644
index 2ab7963..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/TestName.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2019 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi
-
-data class TestName(
- val packageName: String,
- val className: String,
- val methodName: String
-)
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtension.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtension.kt
deleted file mode 100644
index f6f16aa..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtension.kt
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright (C) 2021 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi.accessibility
-
-import android.graphics.drawable.GradientDrawable
-import android.graphics.drawable.LayerDrawable
-import android.util.TypedValue
-import android.view.View
-import android.view.ViewGroup
-import android.widget.LinearLayout
-import android.widget.TextView
-import app.cash.paparazzi.RenderExtension
-import app.cash.paparazzi.accessibility.RenderSettings.DEFAULT_DESCRIPTION_BACKGROUND_COLOR
-import app.cash.paparazzi.accessibility.RenderSettings.DEFAULT_RECT_SIZE
-import app.cash.paparazzi.accessibility.RenderSettings.DEFAULT_RENDER_ALPHA
-import app.cash.paparazzi.accessibility.RenderSettings.DEFAULT_TEXT_COLOR
-import app.cash.paparazzi.accessibility.RenderSettings.DEFAULT_TEXT_SIZE
-import app.cash.paparazzi.accessibility.RenderSettings.getColor
-import app.cash.paparazzi.accessibility.RenderSettings.toColorInt
-import app.cash.paparazzi.accessibility.RenderSettings.withAlpha
-
-class AccessibilityRenderExtension : RenderExtension {
- override fun renderView(
- contentView: View
- ): View {
- val accessibilityViews = contentView.findAccessibilityViews()
- accessibilityViews.forEach { view ->
- val color = getColor(view)
- val colorInt = color.toColorInt()
-
- val colorDrawable = GradientDrawable(
- GradientDrawable.Orientation.TOP_BOTTOM,
- intArrayOf(colorInt, colorInt)
- ).apply {
- setStroke(2, color.withAlpha(DEFAULT_RENDER_ALPHA * 2).toColorInt())
- }
-
- view.foreground = view.foreground?.let { drawable ->
- // If there is an existing foreground layer the color on top of it.
- LayerDrawable(arrayOf(drawable, colorDrawable))
- } ?: colorDrawable
- }
-
- return LinearLayout(contentView.context).apply {
- orientation = LinearLayout.HORIZONTAL
- weightSum = 2f
- layoutParams = ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT
- )
-
- val contentLayoutParams = contentView.layoutParams ?: generateLayoutParams(null)
- addView(
- contentView,
- LinearLayout.LayoutParams(
- contentLayoutParams.width,
- contentLayoutParams.height,
- 1f
- )
- )
- addView(
- buildAccessibilityView(contentView),
- LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT,
- 1f
- )
- )
- }
- }
-
- private fun View.findAccessibilityViews(): List<View> {
- val accessibilityViews = mutableListOf<View>()
- if (isImportantForAccessibility && !iterableTextForAccessibility.isNullOrBlank()) {
- accessibilityViews.add(this)
- }
-
- if (this is ViewGroup) {
- (0 until childCount).forEach {
- accessibilityViews += getChildAt(it).findAccessibilityViews()
- }
- }
-
- return accessibilityViews
- }
-
- private fun buildAccessibilityView(contentView: View): View {
- val linearLayout = LinearLayout(contentView.context).apply {
- orientation = LinearLayout.VERTICAL
- setBackgroundColor(DEFAULT_DESCRIPTION_BACKGROUND_COLOR.toColorInt())
- }
-
- fun renderAccessibility(view: View) {
- if (view.isImportantForAccessibility && !view.iterableTextForAccessibility.isNullOrBlank()) {
- linearLayout.addView(buildAccessibilityRow(view, view.iterableTextForAccessibility))
- }
-
- if (view is ViewGroup) {
- (0 until view.childCount).forEach {
- renderAccessibility(view.getChildAt(it))
- }
- }
- }
-
- renderAccessibility(contentView)
- return linearLayout
- }
-
- private fun buildAccessibilityRow(view: View, iterableTextForAccessibility: CharSequence): View {
- val context = view.context
- val color = getColor(view).toColorInt()
- val margin = view.dip(8)
- val innerMargin = view.dip(4)
-
- return LinearLayout(context).apply {
- orientation = LinearLayout.HORIZONTAL
- layoutParams = ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
- setPaddingRelative(margin, innerMargin, margin, innerMargin)
-
- addView(
- View(context).apply {
- layoutParams = ViewGroup.LayoutParams(dip(DEFAULT_RECT_SIZE), dip(DEFAULT_RECT_SIZE))
- background = GradientDrawable(
- GradientDrawable.Orientation.TOP_BOTTOM,
- intArrayOf(color, color)
- ).apply {
- cornerRadius = dip(DEFAULT_RECT_SIZE / 4f)
- }
- setPaddingRelative(innerMargin, innerMargin, innerMargin, innerMargin)
- }
- )
- addView(
- TextView(context).apply {
- layoutParams = ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
- text = iterableTextForAccessibility
- textSize = DEFAULT_TEXT_SIZE
- setTextColor(DEFAULT_TEXT_COLOR.toColorInt())
- setPaddingRelative(innerMargin, 0, innerMargin, 0)
- }
- )
- }
- }
-}
-
-private fun View.dip(value: Float): Float =
- TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP,
- value,
- resources.displayMetrics
- )
-
-private fun View.dip(value: Int): Int = dip(value.toFloat()).toInt()
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/accessibility/RenderSettings.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/accessibility/RenderSettings.kt
deleted file mode 100644
index 768fc6e..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/accessibility/RenderSettings.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2021 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi.accessibility
-
-import android.view.View
-import java.awt.Color
-
-internal object RenderSettings {
- const val DEFAULT_RENDER_ALPHA = 40
- val DEFAULT_RENDER_COLORS = listOf(
- Color.RED,
- Color.GREEN,
- Color.BLUE,
- Color.YELLOW,
- Color.ORANGE,
- Color.MAGENTA,
- Color.CYAN,
- Color.PINK
- )
- val DEFAULT_TEXT_COLOR: Color = Color.BLACK
- val DEFAULT_DESCRIPTION_BACKGROUND_COLOR: Color = Color.WHITE
- const val DEFAULT_TEXT_SIZE: Float = 10f
- const val DEFAULT_RECT_SIZE: Int = 16
-
- private val colorMap = mutableMapOf<Int, Color>()
-
- fun getColor(view: View): Color {
- val key = "${view::class.simpleName}(${view.iterableTextForAccessibility})"
- return getColor(key)
- }
-
- private fun getColor(key: String): Color {
- val hashCode = key.hashCode()
- return colorMap.getOrPut(hashCode) {
- nextColor(hashCode).withAlpha(DEFAULT_RENDER_ALPHA)
- }
- }
-
- private fun nextColor(hashCode: Int): Color {
- return DEFAULT_RENDER_COLORS[colorIndex(hashCode)]
- }
-
- private fun colorIndex(hashCode: Int): Int {
- val size = DEFAULT_RENDER_COLORS.size
- val i = hashCode % size
- return if (i < 0) i + size else i
- }
-
- internal fun Color.toColorInt(): Int =
- android.graphics.Color.argb(alpha, red, green, blue)
-
- internal fun Color.withAlpha(alpha: Int): Color {
- return Color(red, green, blue, alpha)
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ChoreographerDelegateInterceptor.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ChoreographerDelegateInterceptor.kt
deleted file mode 100644
index 57b7bf1..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ChoreographerDelegateInterceptor.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package app.cash.paparazzi.internal
-
-import android.view.Choreographer
-import com.android.internal.lang.System_Delegate
-
-object ChoreographerDelegateInterceptor {
- @Suppress("unused")
- @JvmStatic
- fun intercept(
- @Suppress("UNUSED_PARAMETER") choreographer: Choreographer
- ): Long = System_Delegate.nanoTime()
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/EditModeInterceptor.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/EditModeInterceptor.kt
deleted file mode 100644
index c464a04..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/EditModeInterceptor.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package app.cash.paparazzi.internal
-
-object EditModeInterceptor {
- @JvmStatic
- fun intercept(): Boolean = false
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/Gc.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/Gc.kt
deleted file mode 100644
index 5bbd25c..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/Gc.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package app.cash.paparazzi.internal
-
-import java.lang.ref.WeakReference
-
-internal object Gc {
- fun gc() {
- // See RuntimeUtil#gc in jlibs (http://jlibs.in/)
- var obj: Any? = Any()
- val ref = WeakReference<Any>(obj)
-
- @Suppress("UNUSED_VAlUE")
- obj = null
- while (ref.get() != null) {
- System.gc()
- System.runFinalization()
- }
-
- System.gc()
- System.runFinalization()
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/IInputMethodManagerInterceptor.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/IInputMethodManagerInterceptor.kt
deleted file mode 100644
index 1908949..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/IInputMethodManagerInterceptor.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package app.cash.paparazzi.internal
-
-import android.os.IBinder
-import com.android.internal.view.IInputMethodManager
-
-/**
- * With [ServiceManagerInterceptor] returning null for the service, we must override the logic
- * in [com.android.internal.view.IInputMethodManager.Stub.asInterface] to return the default
- * implementation of [IInputMethodManager].
- */
-object IInputMethodManagerInterceptor {
- @Suppress("unused")
- @JvmStatic
- fun interceptAsInterface(@Suppress("UNUSED_PARAMETER") obj: IBinder?): IInputMethodManager =
- IInputMethodManager.Default()
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ImageUtils.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ImageUtils.kt
deleted file mode 100644
index 8e7111c..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ImageUtils.kt
+++ /dev/null
@@ -1,357 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package app.cash.paparazzi.internal
-
-import java.awt.AlphaComposite
-import java.awt.Color
-import java.awt.Graphics2D
-import java.awt.RenderingHints.KEY_ANTIALIASING
-import java.awt.RenderingHints.KEY_INTERPOLATION
-import java.awt.RenderingHints.KEY_RENDERING
-import java.awt.RenderingHints.VALUE_ANTIALIAS_ON
-import java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR
-import java.awt.RenderingHints.VALUE_RENDER_QUALITY
-import java.awt.image.BufferedImage
-import java.awt.image.BufferedImage.TYPE_INT_ARGB
-import java.io.File
-import java.io.File.separatorChar
-import java.io.IOException
-import javax.imageio.ImageIO
-import kotlin.math.max
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
-
-/**
- * Utilities related to image processing.
- */
-internal object ImageUtils {
- /**
- * Normally, this test will fail when there is a missing thumbnail. However, when
- * you create creating a new test, it's useful to be able to turn this off such that
- * you can generate all the missing thumbnails in one go, rather than having to run
- * the test repeatedly to get to each new render assertion generating its thumbnail.
- */
- private val FAIL_ON_MISSING_THUMBNAIL = true
-
- private const val THUMBNAIL_SIZE = 1000
-
- /** Directory where to write the thumbnails and deltas. */
- private val failureDir: File
- get() {
- val workingDirString = System.getProperty("user.dir")
- val failureDir = File(workingDirString, "out/failures")
- failureDir.mkdirs()
- return failureDir
- }
-
- @Throws(IOException::class)
- fun requireSimilar(
- relativePath: String,
- image: BufferedImage,
- maxPercentDifference: Double
- ) {
- val scale = getThumbnailScale(image)
- val thumbnail = scale(image, scale, scale)
-
- val `is` = ImageUtils::class.java.classLoader.getResourceAsStream(relativePath)
- if (`is` ==
- null
- ) {
- var message = "Unable to load golden thumbnail: $relativePath\n"
- message = saveImageAndAppendMessage(thumbnail, message, relativePath)
- if (FAIL_ON_MISSING_THUMBNAIL) {
- fail(message)
- } else {
- println(message)
- }
- } else {
- try {
- val goldenImage = ImageIO.read(`is`)
- assertImageSimilar(
- relativePath,
- goldenImage,
- thumbnail,
- maxPercentDifference
- )
- } finally {
- `is`.close()
- }
- }
- }
-
- @Throws(IOException::class)
- fun assertImageSimilar(
- relativePath: String,
- goldenImage: BufferedImage,
- image: BufferedImage,
- maxPercentDifferent: Double
- ) {
- @Suppress("NAME_SHADOWING") var goldenImage = goldenImage
- if (goldenImage.type != TYPE_INT_ARGB) {
- val temp = BufferedImage(
- goldenImage.width,
- goldenImage.height,
- TYPE_INT_ARGB
- )
- temp.graphics.drawImage(goldenImage, 0, 0, null)
- goldenImage = temp
- }
- assertEquals(TYPE_INT_ARGB.toLong(), goldenImage.type.toLong())
-
- val imageWidth = Math.min(goldenImage.width, image.width)
- val imageHeight = Math.min(goldenImage.height, image.height)
-
- // Blur the images to account for the scenarios where there are pixel
- // differences
- // in where a sharp edge occurs
- // goldenImage = blur(goldenImage, 6);
- // image = blur(image, 6);
-
- val width = 3 * imageWidth
- val deltaImage = BufferedImage(width, imageHeight, TYPE_INT_ARGB)
- val g = deltaImage.graphics
-
- // Compute delta map
- var delta: Long = 0
- for (y in 0 until imageHeight) {
- for (x in 0 until imageWidth) {
- val goldenRgb = goldenImage.getRGB(x, y)
- val rgb = image.getRGB(x, y)
- if (goldenRgb == rgb) {
- deltaImage.setRGB(imageWidth + x, y, 0x00808080)
- continue
- }
-
- // If the pixels have no opacity, don't delta colors at all
- if (goldenRgb and -0x1000000 == 0 && rgb and -0x1000000 == 0) {
- deltaImage.setRGB(imageWidth + x, y, 0x00808080)
- continue
- }
-
- val deltaR = (rgb and 0xFF0000).ushr(16) - (goldenRgb and 0xFF0000).ushr(16)
- val newR = 128 + deltaR and 0xFF
- val deltaG = (rgb and 0x00FF00).ushr(8) - (goldenRgb and 0x00FF00).ushr(8)
- val newG = 128 + deltaG and 0xFF
- val deltaB = (rgb and 0x0000FF) - (goldenRgb and 0x0000FF)
- val newB = 128 + deltaB and 0xFF
-
- val avgAlpha =
- ((goldenRgb and -0x1000000).ushr(24) + (rgb and -0x1000000).ushr(24)) / 2 shl 24
-
- val newRGB = avgAlpha or (newR shl 16) or (newG shl 8) or newB
- deltaImage.setRGB(imageWidth + x, y, newRGB)
-
- delta += Math.abs(deltaR)
- .toLong()
- delta += Math.abs(deltaG)
- .toLong()
- delta += Math.abs(deltaB)
- .toLong()
- }
- }
-
- // 3 different colors, 256 color levels
- val total = imageHeight.toLong() * imageWidth.toLong() * 3L * 256L
- val percentDifference = (delta * 100 / total.toDouble()).toFloat()
-
- var error: String? = null
- val imageName = getName(relativePath)
- if (percentDifference > maxPercentDifferent) {
- error = String.format("Images differ (by %.1f%%)", percentDifference)
- } else if (Math.abs(goldenImage.width - image.width) >= 2) {
- error = "Widths differ too much for " + imageName + ": " +
- goldenImage.width + "x" + goldenImage.height +
- "vs" + image.width + "x" + image.height
- } else if (Math.abs(goldenImage.height - image.height) >= 2) {
- error = "Heights differ too much for " + imageName + ": " +
- goldenImage.width + "x" + goldenImage.height +
- "vs" + image.width + "x" + image.height
- }
-
- if (error != null) {
- // Expected on the left
- // Golden on the right
- g.drawImage(goldenImage, 0, 0, null)
- g.drawImage(image, 2 * imageWidth, 0, null)
-
- // Labels
- if (imageWidth > 80) {
- g.color = Color.RED
- g.drawString("Expected", 10, 20)
- g.drawString("Actual", 2 * imageWidth + 10, 20)
- }
-
- val output = File(failureDir, "delta-$imageName")
- if (output.exists()) {
- val deleted = output.delete()
- assertTrue(deleted)
- }
- ImageIO.write(deltaImage, "PNG", output)
- error += " - see details in file://" + output.path + "\n"
- error = saveImageAndAppendMessage(image, error, relativePath)
- println(error)
- fail(error)
- }
-
- g.dispose()
- }
-
- /**
- * Resize the given image
- *
- * @param source the image to be scaled
- * @param xScale x scale
- * @param yScale y scale
- * @return the scaled image
- */
- fun scale(
- source: BufferedImage,
- xScale: Double,
- yScale: Double
- ): BufferedImage {
- @Suppress("NAME_SHADOWING") var source = source
-
- var sourceWidth = source.width
- var sourceHeight = source.height
- val destWidth = Math.max(1, (xScale * sourceWidth).toInt())
- val destHeight = Math.max(1, (yScale * sourceHeight).toInt())
- var imageType = source.type
- if (imageType == BufferedImage.TYPE_CUSTOM) {
- imageType = BufferedImage.TYPE_INT_ARGB
- }
- if (xScale > 0.5 && yScale > 0.5) {
- val scaled = BufferedImage(destWidth, destHeight, imageType)
- val g2 = scaled.createGraphics()
- g2.composite = AlphaComposite.Src
- g2.color = Color(0, true)
- g2.fillRect(0, 0, destWidth, destHeight)
- if (xScale == 1.0 && yScale == 1.0) {
- g2.drawImage(source, 0, 0, null)
- } else {
- setRenderingHints(g2)
- g2.drawImage(source, 0, 0, destWidth, destHeight, 0, 0, sourceWidth, sourceHeight, null)
- }
- g2.dispose()
- return scaled
- } else {
- // When creating a thumbnail, using the above code doesn't work very well;
- // you get some visible artifacts, especially for text. Instead use the
- // technique of repeatedly scaling the image into half; this will cause
- // proper averaging of neighboring pixels, and will typically (for the kinds
- // of screen sizes used by this utility method in the layout editor) take
- // about 3-4 iterations to get the result since we are logarithmically reducing
- // the size. Besides, each successive pass in operating on much fewer pixels
- // (a reduction of 4 in each pass).
- //
- // However, we may not be resizing to a size that can be reached exactly by
- // successively diving in half. Therefore, once we're within a factor of 2 of
- // the final size, we can do a resize to the exact target size.
- // However, we can get even better results if we perform this final resize
- // up front. Let's say we're going from width 1000 to a destination width of 85.
- // The first approach would cause a resize from 1000 to 500 to 250 to 125, and
- // then a resize from 125 to 85. That last resize can distort/blur a lot.
- // Instead, we can start with the destination width, 85, and double it
- // successfully until we're close to the initial size: 85, then 170,
- // then 340, and finally 680. (The next one, 1360, is larger than 1000).
- // So, now we *start* the thumbnail operation by resizing from width 1000 to
- // width 680, which will preserve a lot of visual details such as text.
- // Then we can successively resize the image in half, 680 to 340 to 170 to 85.
- // We end up with the expected final size, but we've been doing an exact
- // divide-in-half resizing operation at the end so there is less distortion.
-
- var iterations = 0 // Number of halving operations to perform after the initial resize
- var nearestWidth = destWidth // Width closest to source width that = 2^x, x is integer
- var nearestHeight = destHeight
- while (nearestWidth < sourceWidth / 2) {
- nearestWidth *= 2
- nearestHeight *= 2
- iterations++
- }
-
- var scaled = BufferedImage(nearestWidth, nearestHeight, imageType)
-
- var g2 = scaled.createGraphics()
- setRenderingHints(g2)
- g2.drawImage(source, 0, 0, nearestWidth, nearestHeight, 0, 0, sourceWidth, sourceHeight, null)
- g2.dispose()
-
- sourceWidth = nearestWidth
- sourceHeight = nearestHeight
- source = scaled
-
- for (iteration in iterations - 1 downTo 0) {
- val halfWidth = sourceWidth / 2
- val halfHeight = sourceHeight / 2
- scaled = BufferedImage(halfWidth, halfHeight, imageType)
- g2 = scaled.createGraphics()
- setRenderingHints(g2)
- g2.drawImage(source, 0, 0, halfWidth, halfHeight, 0, 0, sourceWidth, sourceHeight, null)
- g2.dispose()
-
- sourceWidth = halfWidth
- sourceHeight = halfHeight
- source = scaled
- iterations--
- }
- return scaled
- }
- }
-
- fun getThumbnailScale(image: BufferedImage): Double {
- val maxDimension = max(image.width, image.height)
- return THUMBNAIL_SIZE / maxDimension.toDouble()
- }
-
- private fun setRenderingHints(g2: Graphics2D) {
- g2.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR)
- g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY)
- g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON)
- }
-
- /**
- * Saves the generated thumbnail image and appends the info message to an initial message
- */
- @Throws(IOException::class)
- private fun saveImageAndAppendMessage(
- image: BufferedImage,
- initialMessage: String,
- relativePath: String
- ): String {
- @Suppress("NAME_SHADOWING") var initialMessage = initialMessage
- val output = File(
- failureDir,
- getName(relativePath)
- )
- if (output.exists()) {
- val deleted = output.delete()
- assertTrue(deleted)
- }
- ImageIO.write(image, "PNG", output)
- initialMessage += "Thumbnail for current rendering stored at file://" + output.path
- // initialMessage += "\nRun the following command to accept the changes:\n";
- // initialMessage += String.format("mv %1$s %2$s", output.getPath(),
- // ImageUtils.class.getResource(relativePath).getPath());
- // The above has been commented out, since the destination path returned is in out dir
- // and it makes the tests pass without the code being actually checked in.
- return initialMessage
- }
-
- private fun getName(relativePath: String): String {
- return relativePath.substring(relativePath.lastIndexOf(separatorChar) + 1)
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/MatrixMatrixMultiplicationInterceptor.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/MatrixMatrixMultiplicationInterceptor.kt
deleted file mode 100644
index 41994c2d..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/MatrixMatrixMultiplicationInterceptor.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package app.cash.paparazzi.internal
-
-// Sampled from https://cs.android.com/android/platform/superproject/+/master:external/robolectric-shadows/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOpenGLMatrix.java;l=10-67
-object MatrixMatrixMultiplicationInterceptor {
- @Suppress("unused")
- @JvmStatic
- fun intercept(
- result: FloatArray,
- resultOffset: Int,
- lhs: FloatArray,
- lhsOffset: Int,
- rhs: FloatArray,
- rhsOffset: Int
- ) {
- require(resultOffset + 16 <= result.size) { "resultOffset + 16 > result.length" }
- require(lhsOffset + 16 <= lhs.size) { "lhsOffset + 16 > lhs.length" }
- require(rhsOffset + 16 <= rhs.size) { "rhsOffset + 16 > rhs.length" }
- for (i in 0..3) {
- val rhs_i0 = rhs[I(i, 0, rhsOffset)]
- var ri0 = lhs[I(0, 0, lhsOffset)] * rhs_i0
- var ri1 = lhs[I(0, 1, lhsOffset)] * rhs_i0
- var ri2 = lhs[I(0, 2, lhsOffset)] * rhs_i0
- var ri3 = lhs[I(0, 3, lhsOffset)] * rhs_i0
- for (j in 1..3) {
- val rhs_ij = rhs[I(i, j, rhsOffset)]
- ri0 += lhs[I(j, 0, lhsOffset)] * rhs_ij
- ri1 += lhs[I(j, 1, lhsOffset)] * rhs_ij
- ri2 += lhs[I(j, 2, lhsOffset)] * rhs_ij
- ri3 += lhs[I(j, 3, lhsOffset)] * rhs_ij
- }
- result[I(i, 0, resultOffset)] = ri0
- result[I(i, 1, resultOffset)] = ri1
- result[I(i, 2, resultOffset)] = ri2
- result[I(i, 3, resultOffset)] = ri3
- }
- }
-
- @Suppress("FunctionName")
- private fun I(i: Int, j: Int, offset: Int): Int {
- // #define I(_i, _j) ((_j)+ 4*(_i))
- return offset + j + 4 * i
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/MatrixVectorMultiplicationInterceptor.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/MatrixVectorMultiplicationInterceptor.kt
deleted file mode 100644
index 403413e..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/MatrixVectorMultiplicationInterceptor.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package app.cash.paparazzi.internal
-
-// Sampled from https://cs.android.com/android/platform/superproject/+/master:external/robolectric-shadows/shadows/framework/src/main/java/org/robolectric/shadows/ShadowOpenGLMatrix.java;l=69-121
-object MatrixVectorMultiplicationInterceptor {
- @Suppress("unused")
- @JvmStatic
- fun intercept(
- resultVec: FloatArray,
- resultVecOffset: Int,
- lhsMat: FloatArray,
- lhsMatOffset: Int,
- rhsVec: FloatArray,
- rhsVecOffset: Int
- ) {
- require(resultVecOffset + 4 <= resultVec.size) { "resultOffset + 4 > result.length" }
- require(lhsMatOffset + 16 <= lhsMat.size) { "lhsOffset + 16 > lhs.length" }
- require(rhsVecOffset + 4 <= rhsVec.size) { "rhsOffset + 4 > rhs.length" }
- val x = rhsVec[rhsVecOffset + 0]
- val y = rhsVec[rhsVecOffset + 1]
- val z = rhsVec[rhsVecOffset + 2]
- val w = rhsVec[rhsVecOffset + 3]
- resultVec[resultVecOffset + 0] =
- lhsMat[I(0, 0, lhsMatOffset)] * x + lhsMat[I(1, 0, lhsMatOffset)] * y +
- lhsMat[I(2, 0, lhsMatOffset)] * z + lhsMat[I(3, 0, lhsMatOffset)] * w
- resultVec[resultVecOffset + 1] =
- lhsMat[I(0, 1, lhsMatOffset)] * x + lhsMat[I(1, 1, lhsMatOffset)] * y +
- lhsMat[I(2, 1, lhsMatOffset)] * z + lhsMat[I(3, 1, lhsMatOffset)] * w
- resultVec[resultVecOffset + 2] =
- lhsMat[I(0, 2, lhsMatOffset)] * x + lhsMat[I(1, 2, lhsMatOffset)] * y +
- lhsMat[I(2, 2, lhsMatOffset)] * z + lhsMat[I(3, 2, lhsMatOffset)] * w
- resultVec[resultVecOffset + 3] =
- lhsMat[I(0, 3, lhsMatOffset)] * x + lhsMat[I(1, 3, lhsMatOffset)] * y +
- lhsMat[I(2, 3, lhsMatOffset)] * z + lhsMat[I(3, 3, lhsMatOffset)] * w
- }
-
- @Suppress("FunctionName")
- private fun I(i: Int, j: Int, offset: Int): Int {
- // #define I(_i, _j) ((_j)+ 4*(_i))
- return offset + j + 4 * i
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziAssetRepository.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziAssetRepository.kt
deleted file mode 100644
index 72bcf85..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziAssetRepository.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package app.cash.paparazzi.internal
-
-import com.android.ide.common.rendering.api.AssetRepository
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileNotFoundException
-import java.io.IOException
-import java.io.InputStream
-
-internal class PaparazziAssetRepository(private val assetPath: String) : AssetRepository() {
- @Throws(FileNotFoundException::class)
- private fun open(path: String): InputStream? {
- val asset = File(path)
- return when {
- asset.isFile -> FileInputStream(asset)
- else -> null
- }
- }
-
- override fun isSupported(): Boolean = true
-
- @Throws(IOException::class)
- override fun openAsset(
- path: String,
- mode: Int
- ): InputStream? = open("$assetPath/$path")
-
- @Throws(IOException::class)
- override fun openNonAsset(
- cookie: Int,
- path: String,
- mode: Int
- ): InputStream? = open(path)
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziCallback.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziCallback.kt
deleted file mode 100644
index 6e2b62e..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziCallback.kt
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package app.cash.paparazzi.internal
-
-import app.cash.paparazzi.internal.parsers.LayoutPullParser
-import app.cash.paparazzi.internal.parsers.TagSnapshot
-import com.android.ide.common.rendering.api.ActionBarCallback
-import com.android.ide.common.rendering.api.AdapterBinding
-import com.android.ide.common.rendering.api.ILayoutPullParser
-import com.android.ide.common.rendering.api.LayoutlibCallback
-import com.android.ide.common.rendering.api.ResourceNamespace.RES_AUTO
-import com.android.ide.common.rendering.api.ResourceReference
-import com.android.ide.common.rendering.api.ResourceValue
-import com.android.ide.common.rendering.api.SessionParams.Key
-import com.android.layoutlib.bridge.android.RenderParamsFlags
-import com.android.resources.ResourceType
-import com.android.resources.ResourceType.STYLE
-import com.google.common.io.ByteStreams
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileNotFoundException
-import java.io.IOException
-import java.lang.reflect.Modifier
-import org.kxml2.io.KXmlParser
-import org.xmlpull.v1.XmlPullParser
-import org.xmlpull.v1.XmlPullParserException
-
-internal class PaparazziCallback(
- private val logger: PaparazziLogger,
- private val resourcePackageNames: List<String>
-) : LayoutlibCallback() {
- private val projectResources = mutableMapOf<Int, ResourceReference>()
- private val resources = mutableMapOf<ResourceReference, Int>()
- private val actionBarCallback = ActionBarCallback()
- private val aaptDeclaredResources = mutableMapOf<String, TagSnapshot>()
-
- private var adaptiveIconMaskPath: String? = null
- private val loadedClasses = mutableMapOf<String, Class<*>>()
-
- @Throws(ClassNotFoundException::class)
- fun initResources() {
- for (rPackageName in resourcePackageNames) {
- val rClass = Class.forName("$rPackageName.R")
- for (resourceClass in rClass.declaredClasses) {
- val resourceType = ResourceType.fromClassName(resourceClass.simpleName) ?: continue
-
- for (field in resourceClass.declaredFields) {
- if (!Modifier.isStatic(field.modifiers)) continue
-
- // May not be final in library projects.
- val type = field.type
- try {
- if (type == Int::class.javaPrimitiveType) {
- val value = field.get(null) as Int
- val reference = ResourceReference(RES_AUTO, resourceType, field.name)
- projectResources[value] = reference
- resources[reference] = value
- } else if (type.isArray && type.componentType == Int::class.javaPrimitiveType) {
- // Ignore.
- } else {
- logger.error(null, "Unknown field type in R class: $type")
- }
- } catch (e: IllegalAccessException) {
- logger.error(e, "Malformed R class: %1\$s", "$rPackageName.R")
- }
- }
- }
- }
- }
-
- @Throws(Exception::class)
- override fun loadView(
- name: String,
- constructorSignature: Array<Class<*>>,
- constructorArgs: Array<Any>
- ): Any? {
- val viewClass = Class.forName(name)
- val viewConstructor = viewClass.getConstructor(*constructorSignature)
- viewConstructor.isAccessible = true
- return viewConstructor.newInstance(*constructorArgs)
- }
-
- override fun resolveResourceId(id: Int): ResourceReference? = projectResources[id]
-
- override fun getOrGenerateResourceId(resource: ResourceReference): Int {
- // Workaround: We load our resource map from fields in R.class, which are named using Java
- // class conventions. Therefore, we need to similarly transform style naming conventions
- // that contain periods (e.g., Widget.AppCompat.TextView) to avoid false lookup misses.
- // Long-term: Perhaps parse and load resource names from file system directly?
- val resourceKey =
- if (resource.resourceType == STYLE) resource.transformStyleResource() else resource
- return resources[resourceKey] ?: 0
- }
-
- override fun getParser(layoutResource: ResourceValue): ILayoutPullParser? {
- try {
- val value = layoutResource.value ?: return null
- if (aaptDeclaredResources.isNotEmpty() && layoutResource.resourceType == ResourceType.AAPT) {
- val aaptResource = aaptDeclaredResources.getValue(value)
- return LayoutPullParser.createFromAaptResource(aaptResource)
- }
-
- return LayoutPullParser.createFromFile(File(layoutResource.value))
- .also {
- // For parser of elements included in this parser, publish any aapt declared values
- aaptDeclaredResources.putAll(it.getAaptDeclaredAttrs())
- }
- } catch (e: FileNotFoundException) {
- return null
- }
- }
-
- override fun getAdapterItemValue(
- adapterView: ResourceReference,
- adapterCookie: Any,
- itemRef: ResourceReference,
- fullPosition: Int,
- positionPerType: Int,
- fullParentPosition: Int,
- parentPositionPerType: Int,
- viewRef: ResourceReference,
- viewAttribute: ViewAttribute,
- defaultValue: Any
- ): Any? = null
-
- override fun getAdapterBinding(
- adapterViewRef: ResourceReference,
- adapterCookie: Any,
- viewObject: Any
- ): AdapterBinding? = null
-
- override fun getActionBarCallback(): ActionBarCallback = actionBarCallback
-
- override fun createXmlParserForPsiFile(fileName: String): XmlPullParser? =
- createXmlParserForFile(fileName)
-
- override fun createXmlParserForFile(fileName: String): XmlPullParser? {
- try {
- FileInputStream(fileName).use { fileStream ->
- // Read data fully to memory to be able to close the file stream.
- val byteOutputStream = ByteArrayOutputStream()
- ByteStreams.copy(fileStream, byteOutputStream)
- val parser = KXmlParser()
- parser.setInput(ByteArrayInputStream(byteOutputStream.toByteArray()), null)
- parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true)
- return parser
- }
- } catch (e: IOException) {
- return null
- } catch (e: XmlPullParserException) {
- return null
- }
- }
-
- override fun createXmlParser(): XmlPullParser = KXmlParser()
-
- @Suppress("UNCHECKED_CAST")
- override fun <T> getFlag(key: Key<T>?): T? {
- return when (key) {
- RenderParamsFlags.FLAG_KEY_ADAPTIVE_ICON_MASK_PATH -> adaptiveIconMaskPath as T?
- else -> null
- }
- }
-
- fun setAdaptiveIconMaskPath(adaptiveIconMaskPath: String) {
- this.adaptiveIconMaskPath = adaptiveIconMaskPath
- }
-
- override fun findClass(name: String): Class<*> {
- val clazz = loadedClasses[name]
- logger.verbose("loadClassA($name)")
-
- try {
- if (clazz != null) {
- return clazz
- }
- val clazz2 = Class.forName(name)
- logger.verbose("loadClassB($name)")
- loadedClasses[name] = clazz2
- return clazz2
- } catch (e: LinkageError) {
- throw ClassNotFoundException("error loading class $name", e)
- } catch (e: ExceptionInInitializerError) {
- throw ClassNotFoundException("error loading class $name", e)
- } catch (e: ClassNotFoundException) {
- throw ClassNotFoundException("error loading class $name", e)
- }
- }
-
- private fun ResourceReference.transformStyleResource() =
- ResourceReference.style(namespace, name.replace('.', '_'))
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziJson.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziJson.kt
deleted file mode 100644
index 6739e81..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziJson.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2019 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi.internal
-
-import app.cash.paparazzi.Snapshot
-import app.cash.paparazzi.TestName
-import com.squareup.moshi.FromJson
-import com.squareup.moshi.JsonAdapter
-import com.squareup.moshi.Moshi
-import com.squareup.moshi.ToJson
-import com.squareup.moshi.Types
-import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
-import java.util.Date
-
-internal object PaparazziJson {
- val moshi = Moshi.Builder()
- .add(Date::class.java, Rfc3339DateJsonAdapter())
- .add(this)
- .build()!!
-
- val listOfShotsAdapter: JsonAdapter<List<Snapshot>> =
- moshi
- .adapter<List<Snapshot>>(
- Types.newParameterizedType(List::class.java, Snapshot::class.java)
- )
- .indent(" ")
-
- val listOfStringsAdapter: JsonAdapter<List<String>> =
- moshi
- .adapter<List<String>>(
- Types.newParameterizedType(List::class.java, String::class.java)
- )
- .indent(" ")
-
- @ToJson
- fun testNameToJson(testName: TestName): String {
- return "${testName.packageName}.${testName.className}#${testName.methodName}"
- }
-
- @FromJson
- fun testNameFromJson(json: String): TestName {
- val regex = Regex("(.*)\\.([^.]*)#([^.]*)")
- val (packageName, className, methodName) = regex.matchEntire(json)!!.destructured
- return TestName(packageName, className, methodName)
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziLogger.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziLogger.kt
deleted file mode 100644
index 01595f7..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/PaparazziLogger.kt
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright (C) 2019 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi.internal
-
-import app.cash.paparazzi.Paparazzi
-import com.android.ide.common.rendering.api.ILayoutLog
-import com.android.utils.ILogger
-import java.io.PrintStream
-import java.io.PrintWriter
-import java.util.logging.Level
-import java.util.logging.Logger
-import java.util.logging.Logger.getLogger
-
-/**
- * This logger delegates to java.util.Logging.
- */
-internal class PaparazziLogger : ILayoutLog, ILogger {
- private val logger: Logger = getLogger(Paparazzi::class.java.name)
- private val errors = mutableListOf<Throwable>()
-
- override fun error(
- throwable: Throwable?,
- format: String?,
- vararg args: Any
- ) {
- logger.log(Level.SEVERE, format?.format(args), throwable)
- if (throwable != null) {
- errors += throwable
- }
- }
-
- override fun warning(
- format: String,
- vararg args: Any
- ) {
- logger.log(Level.WARNING, format, args)
- }
-
- override fun info(
- format: String,
- vararg args: Any
- ) {
- logger.log(Level.INFO, format, args)
- }
-
- override fun verbose(
- format: String,
- vararg args: Any
- ) {
- logger.log(Level.FINE, format, args)
- }
-
- override fun fidelityWarning(
- tag: String?,
- message: String?,
- throwable: Throwable?,
- cookie: Any?,
- data: Any?
- ) {
- logger.log(Level.WARNING, "$tag: $message", throwable)
- }
-
- override fun error(
- tag: String?,
- message: String?,
- viewCookie: Any?,
- data: Any?
- ) {
- logger.log(Level.SEVERE, "$tag: $message")
- }
-
- override fun error(
- tag: String?,
- message: String?,
- throwable: Throwable?,
- viewCookie: Any?,
- data: Any?
- ) {
- logger.log(Level.SEVERE, "$tag: $message", throwable)
- if (throwable != null) {
- errors += throwable
- }
- }
-
- override fun warning(
- tag: String?,
- message: String?,
- viewCookie: Any?,
- data: Any?
- ) {
- logger.log(Level.WARNING, "$tag: $message")
- }
-
- override fun logAndroidFramework(priority: Int, tag: String?, message: String?) {
- logger.log(Level.INFO, "$tag [$priority]: $message")
- }
-
- fun assertNoErrors() {
- when (errors.size) {
- 0 -> return
- 1 -> throw errors[0]
- else -> throw MultipleFailuresException(errors)
- }
- }
-
- internal class MultipleFailuresException(private val causes: List<Throwable>) : Exception() {
- init {
- require(causes.isNotEmpty()) { "List of Throwables must not be empty" }
- }
-
- override val message: String
- get() = buildString {
- appendLine(String.format("There were %d errors:", causes.size))
- causes.forEach { e ->
- appendLine(String.format("%n %s: %s", e.javaClass.name, e.message))
- e.stackTrace.forEach { traceElement ->
- appendLine("\tat $traceElement")
- }
- }
- }
-
- override fun printStackTrace() {
- causes.forEach { e ->
- e.printStackTrace()
- }
- }
-
- override fun printStackTrace(s: PrintStream) {
- causes.forEach { e ->
- e.printStackTrace(s)
- }
- }
-
- override fun printStackTrace(s: PrintWriter) {
- causes.forEach { e ->
- e.printStackTrace(s)
- }
- }
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/RenderResult.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/RenderResult.kt
deleted file mode 100644
index cbf05df..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/RenderResult.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package app.cash.paparazzi.internal
-
-import com.android.ide.common.rendering.api.RenderSession
-import com.android.ide.common.rendering.api.Result
-import com.android.ide.common.rendering.api.ViewInfo
-import java.awt.image.BufferedImage
-
-internal data class RenderResult(
- val result: Result,
- val systemViews: List<ViewInfo>,
- val rootViews: List<ViewInfo>,
- val image: BufferedImage
-)
-
-internal fun RenderSession.toResult(): RenderResult {
- return RenderResult(result, systemRootViews.toList(), rootViews.toList(), image)
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/Renderer.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/Renderer.kt
deleted file mode 100644
index 569d1cd..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/Renderer.kt
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package app.cash.paparazzi.internal
-
-import app.cash.paparazzi.DeviceConfig
-import app.cash.paparazzi.Environment
-import app.cash.paparazzi.Flags
-import app.cash.paparazzi.internal.parsers.LayoutPullParser
-import com.android.ide.common.rendering.api.SessionParams
-import com.android.io.FolderWrapper
-import com.android.layoutlib.bridge.Bridge
-import com.android.layoutlib.bridge.android.RenderParamsFlags
-import com.android.layoutlib.bridge.impl.DelegateManager
-import java.awt.image.BufferedImage
-import java.io.Closeable
-import java.io.File
-import java.io.IOException
-import java.util.Locale
-
-/** View rendering. */
-internal class Renderer(
- private val environment: Environment,
- private val layoutlibCallback: PaparazziCallback,
- private val logger: PaparazziLogger,
- private val maxPercentDifference: Double
-) : Closeable {
- private var bridge: Bridge? = null
- private lateinit var sessionParamsBuilder: SessionParamsBuilder
-
- /** Initialize the bridge and the resource maps. */
- fun prepare(): SessionParamsBuilder {
- val platformDataResDir = File("${environment.platformDir}/data/res")
-
- @Suppress("DEPRECATION")
- val frameworkResources = com.android.ide.common.resources.deprecated.FrameworkResources(
- FolderWrapper(platformDataResDir)
- ).apply {
- loadResources()
- loadPublicResources(logger)
- }
-
- @Suppress("DEPRECATION")
- val projectResources = object : com.android.ide.common.resources.deprecated.ResourceRepository(
- FolderWrapper(environment.resDir),
- false
- ) {
- override fun createResourceItem(
- name: String
- ): com.android.ide.common.resources.deprecated.ResourceItem {
- return com.android.ide.common.resources.deprecated.ResourceItem(name)
- }
- }
- projectResources.loadResources()
-
- sessionParamsBuilder = SessionParamsBuilder(
- layoutlibCallback = layoutlibCallback,
- logger = logger,
- frameworkResources = frameworkResources,
- projectResources = projectResources,
- assetRepository = PaparazziAssetRepository(environment.assetsDir)
- )
- .plusFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE, true)
- .withTheme("AppTheme", true)
-
- val platformDataRoot = System.getProperty("paparazzi.platform.data.root")
- ?: throw RuntimeException("Missing system property for 'paparazzi.platform.data.root'")
- val platformDataDir = File(platformDataRoot, "data")
- val fontLocation = File(platformDataDir, "fonts")
- val nativeLibLocation = File(platformDataDir, getNativeLibDir())
- val icuLocation = File(platformDataDir, "icu" + File.separator + "icudt70l.dat")
- val buildProp = File(environment.platformDir, "build.prop")
- val attrs = File(platformDataResDir, "values" + File.separator + "attrs.xml")
- val systemProperties = DeviceConfig.loadProperties(buildProp) + mapOf(
- // We want Choreographer.USE_FRAME_TIME to be false so it uses System_Delegate.nanoTime()
- "debug.choreographer.frametime" to "false"
- )
- bridge = Bridge().apply {
- check(
- init(
- systemProperties,
- fontLocation,
- nativeLibLocation.path,
- icuLocation.path,
- DeviceConfig.getEnumMap(attrs),
- logger
- )
- ) { "Failed to init Bridge." }
- }
- Bridge.getLock()
- .lock()
- try {
- Bridge.setLog(logger)
- } finally {
- Bridge.getLock()
- .unlock()
- }
-
- return sessionParamsBuilder
- }
-
- private fun getNativeLibDir(): String {
- val osName = System.getProperty("os.name").lowercase(Locale.US)
- val osLabel = when {
- osName.startsWith("windows") -> "win"
- osName.startsWith("mac") -> {
- val osArch = System.getProperty("os.arch").lowercase(Locale.US)
- if (osArch.startsWith("x86")) "mac" else "mac-arm"
- }
- else -> "linux"
- }
- return "$osLabel/lib64"
- }
-
- override fun close() {
- bridge = null
-
- Gc.gc()
-
- dumpDelegates()
- }
-
- fun dumpDelegates() {
- if (System.getProperty(Flags.DEBUG_LINKED_OBJECTS) != null) {
- println("Objects still linked from the DelegateManager:")
- DelegateManager.dump(System.out)
- }
- }
-
- fun render(
- bridge: com.android.ide.common.rendering.api.Bridge,
- params: SessionParams,
- frameTimeNanos: Long
- ): RenderResult {
- val session = bridge.createSession(params)
-
- try {
- if (frameTimeNanos != -1L) {
- session.setElapsedFrameTimeNanos(frameTimeNanos)
- }
-
- if (!session.result.isSuccess) {
- logger.error(session.result.exception, session.result.errorMessage)
- } else {
- // Render the session with a timeout of 50s.
- val renderResult = session.render(50000)
- if (!renderResult.isSuccess) {
- logger.error(session.result.exception, session.result.errorMessage)
- }
- }
-
- return session.toResult()
- } finally {
- session.dispose()
- }
- }
-
- /** Compares the golden image with the passed image. */
- fun verify(
- goldenImageName: String,
- image: BufferedImage
- ) {
- try {
- val goldenImagePath = environment.appTestDir + "/golden/" + goldenImageName
- ImageUtils.requireSimilar(goldenImagePath, image, maxPercentDifference)
- } catch (e: IOException) {
- logger.error(e, e.message)
- }
- }
-
- /**
- * Create a new rendering session and test that rendering the given layout doesn't throw any
- * exceptions and matches the provided image.
- *
- * If frameTimeNanos is >= 0 a frame will be executed during the rendering. The time indicates
- * how far in the future is.
- */
- @JvmOverloads
- fun renderAndVerify(
- sessionParams: SessionParams,
- goldenFileName: String,
- frameTimeNanos: Long = -1
- ): RenderResult {
- val result = render(bridge!!, sessionParams, frameTimeNanos)
- verify(goldenFileName, result.image)
- return result
- }
-
- fun createParserFromPath(layoutPath: String): LayoutPullParser =
- LayoutPullParser.createFromPath("${environment.resDir}/layout/$layoutPath")
-
- /**
- * Create a new rendering session and test that rendering the given layout on given device
- * doesn't throw any exceptions and matches the provided image.
- */
- @JvmOverloads
- fun renderAndVerify(
- layoutFileName: String,
- goldenFileName: String,
- deviceConfig: DeviceConfig = DeviceConfig.NEXUS_5
- ): RenderResult {
- val sessionParams = sessionParamsBuilder
- .copy(
- layoutPullParser = createParserFromPath(layoutFileName),
- deviceConfig = deviceConfig
- )
- .build()
- return renderAndVerify(sessionParams, goldenFileName)
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ResourcesInterceptor.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ResourcesInterceptor.kt
deleted file mode 100644
index a7f14c8..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ResourcesInterceptor.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package app.cash.paparazzi.internal
-
-import android.content.Context
-import android.graphics.Typeface
-
-object ResourcesInterceptor {
- @JvmStatic
- fun intercept(
- context: Context,
- resId: Int
- ): Typeface? {
- return context.resources.getFont(resId)
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ServiceManagerInterceptor.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ServiceManagerInterceptor.kt
deleted file mode 100644
index 4726fad..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/ServiceManagerInterceptor.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package app.cash.paparazzi.internal
-
-import android.os.IBinder
-
-/**
- * The ImeTracing class attempts to initialize its [mService field in its constructor](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/imetracing/ImeTracing.java;l=60).
- *
- * Unfortunately, [layoutlib's version of ServiceManager](https://cs.android.com/android/platform/superproject/+/master:frameworks/layoutlib/bridge/src/android/os/ServiceManager.java;l=37)
- * throws an exception immediately.
- *
- * This interceptor overrides ServiceManager.getServiceOrThrow to simply return null instead.
- */
-object ServiceManagerInterceptor {
- @Suppress("unused")
- @JvmStatic
- fun interceptGetServiceOrThrow(@Suppress("UNUSED_PARAMETER") name: String): IBinder? = null
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/SessionParamsBuilder.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/SessionParamsBuilder.kt
deleted file mode 100644
index ea263b6..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/SessionParamsBuilder.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package app.cash.paparazzi.internal
-
-import app.cash.paparazzi.DeviceConfig
-import app.cash.paparazzi.internal.parsers.LayoutPullParser
-import com.android.SdkConstants
-import com.android.ide.common.rendering.api.AssetRepository
-import com.android.ide.common.rendering.api.ResourceNamespace
-import com.android.ide.common.rendering.api.ResourceReference
-import com.android.ide.common.rendering.api.SessionParams
-import com.android.ide.common.rendering.api.SessionParams.Key
-import com.android.ide.common.rendering.api.SessionParams.RenderingMode
-import com.android.ide.common.resources.ResourceResolver
-import com.android.ide.common.resources.ResourceValueMap
-import com.android.layoutlib.bridge.Bridge
-import com.android.resources.LayoutDirection
-import com.android.resources.ResourceType
-
-/** Creates [SessionParams] objects. */
-internal data class SessionParamsBuilder(
- private val layoutlibCallback: PaparazziCallback,
- private val logger: PaparazziLogger,
- @Suppress("DEPRECATION")
- private val frameworkResources: com.android.ide.common.resources.deprecated.ResourceRepository,
- private val assetRepository: AssetRepository,
- @Suppress("DEPRECATION")
- private val projectResources: com.android.ide.common.resources.deprecated.ResourceRepository,
- private val deviceConfig: DeviceConfig = DeviceConfig.NEXUS_5,
- private val renderingMode: RenderingMode = RenderingMode.NORMAL,
- private val targetSdk: Int = 22,
- private val flags: Map<Key<*>, Any> = mapOf(),
- private val themeName: String? = null,
- private val isProjectTheme: Boolean = false,
- private val layoutPullParser: LayoutPullParser? = null,
- private val projectKey: Any? = null,
- private val minSdk: Int = 0,
- private val decor: Boolean = true
-) {
- fun withTheme(
- themeName: String,
- isProjectTheme: Boolean
- ): SessionParamsBuilder {
- return copy(themeName = themeName, isProjectTheme = isProjectTheme)
- }
-
- fun withTheme(themeName: String): SessionParamsBuilder {
- return when {
- themeName.startsWith(SdkConstants.PREFIX_ANDROID) -> {
- withTheme(themeName.substring(SdkConstants.PREFIX_ANDROID.length), false)
- }
- else -> withTheme(themeName, true)
- }
- }
-
- fun plusFlag(
- flag: SessionParams.Key<*>,
- value: Any
- ) = copy(flags = flags + (flag to value))
-
- fun build(): SessionParams {
- require(themeName != null)
-
- val folderConfiguration = deviceConfig.folderConfiguration
-
- @Suppress("DEPRECATION")
- val resourceResolver = ResourceResolver.create(
- mapOf<ResourceNamespace, Map<ResourceType, ResourceValueMap>>(
- ResourceNamespace.ANDROID to frameworkResources.getConfiguredResources(
- folderConfiguration
- ),
- ResourceNamespace.TODO() to projectResources.getConfiguredResources(
- folderConfiguration
- )
- ),
- ResourceReference(
- ResourceNamespace.fromBoolean(!isProjectTheme),
- ResourceType.STYLE,
- themeName
- )
- )
-
- val result = SessionParams(
- layoutPullParser, renderingMode, projectKey /* for caching */,
- deviceConfig.hardwareConfig, resourceResolver, layoutlibCallback, minSdk, targetSdk, logger
- )
- result.fontScale = deviceConfig.fontScale
-
- val localeQualifier = folderConfiguration.localeQualifier
- val layoutDirectionQualifier = folderConfiguration.layoutDirectionQualifier
- // https://cs.android.com/android-studio/platform/tools/adt/idea/+/mirror-goog-studio-main:android/src/com/android/tools/idea/rendering/RenderTask.java;l=645
- if (
- LayoutDirection.RTL == layoutDirectionQualifier.value &&
- !Bridge.isLocaleRtl(localeQualifier.tag)
- ) {
- result.locale = "ur"
- } else {
- result.locale = localeQualifier.tag
- }
- result.setRtlSupport(true)
-
- for ((key, value) in flags) {
- @Suppress("UNCHECKED_CAST")
- result.setFlag(key as Key<Any>, value)
- }
- result.setAssetRepository(assetRepository)
-
- if (!decor) {
- result.setForceNoDecor()
- }
-
- return result
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrParser.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrParser.kt
deleted file mode 100644
index 3c62aa8..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrParser.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2021 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi.internal.parsers
-
-/**
- * Copied from https://cs.android.com/android-studio/platform/tools/adt/idea/+/858f81bb7c350bc7a05daad36edefd21f74c8cef:android/src/com/android/tools/idea/rendering/parsers/AaptAttrParser.java
- *
- * Interface for parsers that support declaration of inlined {@code aapt:attr} attributes
- */
-interface AaptAttrParser {
- /**
- * Returns a [Map] that contains all the `aapt:attr` elements declared in this or any
- * children parsers. This list can be used to resolve `@aapt/_aapt` references into this parser.
- */
- fun getAaptDeclaredAttrs(): Map<String, TagSnapshot>
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrSnapshot.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrSnapshot.kt
deleted file mode 100644
index 4105eee..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AaptAttrSnapshot.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2021 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi.internal.parsers
-
-import com.android.SdkConstants.AAPT_ATTR_PREFIX
-import com.android.SdkConstants.AAPT_PREFIX
-
-/**
- * Derived from https://cs.android.com/android-studio/platform/tools/adt/idea/+/mirror-goog-studio-master-dev:android/src/com/android/tools/idea/rendering/parsers/AaptAttrAttributeSnapshot.java
- *
- * Aapt attributes are attributes that instead of containing a reference, contain the inlined value
- * of the reference. This snapshot will generate a dynamic reference that will be used by the
- * resource resolution to be able to retrieve the inlined value.
- */
-class AaptAttrSnapshot(
- override val namespace: String,
- override val prefix: String,
- override val name: String,
- val id: String,
- val bundledTag: TagSnapshot
-) : AttributeSnapshot(namespace, prefix, name, "${AAPT_ATTR_PREFIX}$AAPT_PREFIX$id")
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AttributeSnapshot.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AttributeSnapshot.kt
deleted file mode 100644
index 50b658c..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/AttributeSnapshot.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2021 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi.internal.parsers
-
-/**
- * Derived from https://cs.android.com/android-studio/platform/tools/adt/idea/+/mirror-goog-studio-master-dev:android/src/com/android/tools/idea/rendering/parsers/AttributeSnapshot.java
- *
- * A snapshot of an attribute value pulled from an XML resource.
- * Used in conjunction with [TagSnapshot].
- */
-open class AttributeSnapshot(
- open val namespace: String,
- open val prefix: String?,
- open val name: String,
- open val value: String
-) {
- override fun toString() = "$name: $value"
-
- // since data classes can't be subclassed
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as AttributeSnapshot
-
- if (namespace != other.namespace) return false
- if (prefix != other.prefix) return false
- if (name != other.name) return false
- if (value != other.value) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = namespace.hashCode()
- result = 31 * result + prefix.hashCode()
- result = 31 * result + name.hashCode()
- result = 31 * result + value.hashCode()
- return result
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/InMemoryParser.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/InMemoryParser.kt
deleted file mode 100644
index 5b709fb..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/InMemoryParser.kt
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright (C) 2021 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi.internal.parsers
-
-import org.kxml2.io.KXmlParser
-import org.xmlpull.v1.XmlPullParserException
-
-/**
- * Derived from https://cs.android.com/android-studio/platform/tools/adt/idea/+/858f81bb7c350bc7a05daad36edefd21f74c8cef:android/src/com/android/tools/idea/rendering/parsers/LayoutPullParser.java;bpv=0;bpt=0
- *
- * A parser implementation that walks an in-memory XML DOM tree.
- */
-abstract class InMemoryParser : KXmlParser() {
- abstract fun rootTag(): TagSnapshot
-
- private val nodeStack = mutableListOf<TagSnapshot>()
- private var parsingState = START_DOCUMENT
-
- override fun getAttributeCount(): Int {
- val tag = getCurrentNode() ?: return 0
- return tag.attributes.size
- }
-
- override fun getAttributeName(i: Int): String? {
- val attribute = getAttribute(i) ?: return null
- return attribute.name
- }
-
- override fun getAttributeNamespace(i: Int): String {
- val attribute = getAttribute(i) ?: return ""
- return attribute.namespace
- }
-
- override fun getAttributePrefix(i: Int): String? {
- val attribute = getAttribute(i) ?: return null
- return attribute.prefix
- }
-
- override fun getAttributeValue(i: Int): String? {
- val attribute = getAttribute(i) ?: return null
- return attribute.value
- }
-
- override fun getAttributeValue(
- namespace: String?,
- name: String?
- ): String? {
- val tag = getCurrentNode() ?: return null
- return tag.attributes.find { it.name == name }?.value
- }
-
- override fun getDepth(): Int = nodeStack.size
-
- override fun getName(): String? {
- if (parsingState == START_TAG || parsingState == END_TAG) {
- // Should only be called when START_TAG
- val currentNode = getCurrentNode()!!
- return currentNode.name
- }
- return null
- }
-
- @Throws(XmlPullParserException::class)
- override fun next(): Int {
- when (parsingState) {
- END_DOCUMENT -> throw XmlPullParserException("Nothing after the end")
- START_DOCUMENT -> onNextFromStartDocument()
- START_TAG -> onNextFromStartTag()
- END_TAG -> onNextFromEndTag()
- }
- return parsingState
- }
-
- private fun getCurrentNode(): TagSnapshot? = nodeStack.lastOrNull()
-
- private fun getAttribute(i: Int): AttributeSnapshot? {
- if (parsingState != START_TAG) {
- throw IndexOutOfBoundsException()
- }
- val tag = getCurrentNode() ?: return null
- return tag.attributes[i]
- }
-
- private fun push(node: TagSnapshot) {
- nodeStack.add(node)
- }
-
- private fun pop(): TagSnapshot = nodeStack.removeLast()
-
- private fun onNextFromStartDocument() {
- val rootTag = rootTag()
- @Suppress("SENSELESS_COMPARISON")
- parsingState = if (rootTag != null) {
- push(rootTag)
- START_TAG
- } else {
- END_DOCUMENT
- }
- }
-
- private fun onNextFromStartTag() {
- // get the current node, and look for text or children (children first)
- // Should only be called when START_TAG
- val node = getCurrentNode()!!
- val children = node.children
- parsingState = if (children.isNotEmpty()) {
- // move to the new child, and don't change the state.
- push(children[0])
-
- // in case the current state is CURRENT_DOC, we set the proper state.
- START_TAG
- } else {
- if (parsingState == START_DOCUMENT) {
- // this handles the case where there's no node.
- END_DOCUMENT
- } else {
- END_TAG
- }
- }
- }
-
- private fun onNextFromEndTag() {
- // look for a sibling. if no sibling, go back to the parent
- // Should only be called when END_TAG
- var node = getCurrentNode()!!
- val sibling = node.next
- if (sibling != null) {
- node = sibling
- // to go to the sibling, we need to remove the current node,
- pop()
- // and add its sibling.
- push(node)
- parsingState = START_TAG
- } else {
- // move back to the parent
- pop()
-
- // we have only one element left (myRoot), then we're done with the document.
- parsingState = if (nodeStack.isEmpty()) {
- END_DOCUMENT
- } else {
- END_TAG
- }
- }
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/LayoutPullParser.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/LayoutPullParser.kt
deleted file mode 100644
index 8f1a94a..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/LayoutPullParser.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package app.cash.paparazzi.internal.parsers
-
-import com.android.SdkConstants.ATTR_IGNORE
-import com.android.SdkConstants.EXPANDABLE_LIST_VIEW
-import com.android.SdkConstants.GRID_VIEW
-import com.android.SdkConstants.LIST_VIEW
-import com.android.SdkConstants.SPINNER
-import com.android.SdkConstants.TOOLS_URI
-import com.android.ide.common.rendering.api.ILayoutPullParser
-import com.android.ide.common.rendering.api.ResourceNamespace
-import java.io.ByteArrayInputStream
-import java.io.File
-import java.io.FileInputStream
-import java.io.FileNotFoundException
-import java.io.IOError
-import java.io.InputStream
-import java.nio.charset.Charset
-import okio.buffer
-import okio.source
-import org.xmlpull.v1.XmlPullParserException
-
-/**
- * A layout parser that holds an in-memory tree of a given resource for subsequent traversal
- * during the inflation process
- */
-internal class LayoutPullParser : InMemoryParser, AaptAttrParser, ILayoutPullParser {
- private constructor(inputStream: InputStream) : super() {
- try {
- val buffer = inputStream.source().buffer()
-
- setFeature(FEATURE_PROCESS_NAMESPACES, true)
- setInput(buffer.peek().inputStream(), null)
-
- // IntelliJ uses XmlFile/PsiFile to parse tag snapshots,
- // leaving XmlPullParser for Android to parse resources as usual
- // Here, we use the same XmlPullParser approach for both, which means
- // we need reinitialize the document stream between the two passes.
- val resourceParser = ResourceParser(buffer.inputStream())
- root = resourceParser.createTagSnapshot()
-
- // Obtain a list of all the aapt declared attributes
- declaredAaptAttrs = findDeclaredAaptAttrs(root)
- } catch (e: XmlPullParserException) {
- throw IOError(e)
- }
- }
-
- private constructor(aaptResource: TagSnapshot) : super() {
- root = aaptResource
- declaredAaptAttrs = emptyMap()
- }
-
- private val root: TagSnapshot
- private val declaredAaptAttrs: Map<String, TagSnapshot>
-
- private var layoutNamespace = ResourceNamespace.RES_AUTO
-
- override fun rootTag() = root
-
- @Suppress("SENSELESS_COMPARISON")
- override fun getViewCookie(): Any? {
- // TODO: Implement this properly.
- val name = super.getName() ?: return null
-
- // Store tools attributes if this looks like a layout we'll need adapter view
- // bindings for in the LayoutlibCallback.
- if (LIST_VIEW == name || EXPANDABLE_LIST_VIEW == name || GRID_VIEW == name || SPINNER == name) {
- var map: MutableMap<String, String>? = null
- val count = attributeCount
- for (i in 0 until count) {
- val namespace = getAttributeNamespace(i)
- if (namespace != null && namespace == TOOLS_URI) {
- val attribute = getAttributeName(i)!!
- if (attribute == ATTR_IGNORE) {
- continue
- }
- if (map == null) {
- map = HashMap(4)
- }
- map[attribute] = getAttributeValue(i)!!
- }
- }
-
- return map
- }
-
- return null
- }
-
- override fun getLayoutNamespace(): ResourceNamespace = layoutNamespace
-
- override fun getAaptDeclaredAttrs(): Map<String, TagSnapshot> = declaredAaptAttrs
-
- fun setLayoutNamespace(layoutNamespace: ResourceNamespace) {
- this.layoutNamespace = layoutNamespace
- }
-
- private fun findDeclaredAaptAttrs(tag: TagSnapshot): Map<String, TagSnapshot> {
- if (!tag.hasDeclaredAaptAttrs) {
- // Nor tag or any of the children has any aapt:attr declarations, we can stop here.
- return emptyMap()
- }
-
- return buildMap {
- tag.attributes
- .filterIsInstance<AaptAttrSnapshot>()
- .forEach { attr ->
- val bundledTag = attr.bundledTag
- put(attr.id, bundledTag)
- for (child in bundledTag.children) {
- putAll(findDeclaredAaptAttrs(child))
- }
- }
- for (child in tag.children) {
- putAll(findDeclaredAaptAttrs(child))
- }
- }
- }
-
- companion object {
- @Throws(FileNotFoundException::class)
- fun createFromFile(layoutFile: File) = LayoutPullParser(FileInputStream(layoutFile))
-
- /**
- * @param layoutPath Must start with '/' and be relative to test resources.
- */
- fun createFromPath(layoutPath: String): LayoutPullParser {
- @Suppress("NAME_SHADOWING") var layoutPath = layoutPath
- if (layoutPath.startsWith("/")) {
- layoutPath = layoutPath.substring(1)
- }
-
- return LayoutPullParser(
- LayoutPullParser::class.java.classLoader!!.getResourceAsStream(layoutPath)
- )
- }
-
- fun createFromString(contents: String): LayoutPullParser {
- return LayoutPullParser(
- ByteArrayInputStream(contents.toByteArray(Charset.forName("UTF-8")))
- )
- }
-
- fun createFromAaptResource(aaptResource: TagSnapshot): LayoutPullParser {
- return LayoutPullParser(aaptResource)
- }
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/ResourceParser.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/ResourceParser.kt
deleted file mode 100644
index f90475b..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/ResourceParser.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (C) 2021 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi.internal.parsers
-
-import com.android.SdkConstants.AAPT_URI
-import com.android.SdkConstants.TAG_ATTR
-import java.io.InputStream
-import org.kxml2.io.KXmlParser
-
-/**
- * An XML resource parser that creates a tree of [TagSnapshot]s
- */
-class ResourceParser(inputStream: InputStream) : KXmlParser() {
- init {
- setFeature(FEATURE_PROCESS_NAMESPACES, true)
- setInput(inputStream, null)
-
- require(START_DOCUMENT, null, null)
- next()
- }
-
- fun createTagSnapshot(): TagSnapshot {
- require(START_TAG, null, null)
-
- // need to store now, since TagSnapshot is created on end tag after parser mark has moved
- val tagName = name
- val tagNamespace = namespace
- val prefix = prefix
-
- val attributes = createAttributesForTag()
-
- var hasDeclaredAaptAttrs = false
- var last: TagSnapshot? = null
- val children = mutableListOf<TagSnapshot>()
- while (eventType != END_DOCUMENT) {
- when (next()) {
- START_TAG -> {
- if (AAPT_URI == namespace) {
- if (TAG_ATTR == name) {
- val attrAttribute = createAttrTagSnapshot()
- if (attrAttribute != null) {
- attributes += attrAttribute
- hasDeclaredAaptAttrs = true
- }
- }
- // Since we save the aapt:attr tags as an attribute, we do not save them as a child element. Skip.
- } else {
- val child = createTagSnapshot()
- hasDeclaredAaptAttrs = hasDeclaredAaptAttrs || child.hasDeclaredAaptAttrs
- children += child
- if (last != null) {
- last.next = child
- }
- last = child
- }
- }
- END_TAG -> {
- return TagSnapshot(
- tagName,
- tagNamespace,
- prefix,
- attributes,
- children.toList(),
- hasDeclaredAaptAttrs
- )
- }
- }
- }
-
- throw IllegalStateException("We should never reach here")
- }
-
- private fun createAttrTagSnapshot(): AaptAttrSnapshot? {
- require(START_TAG, null, "attr")
-
- val name = getAttributeValue(null, "name") ?: return null
- val prefix = findPrefixByQualifiedName(name)
- val namespace = getNamespace(prefix)
- val localName = findLocalNameByQualifiedName(name)
- val id = (++uniqueId).toString()
-
- var bundleTagSnapshot: TagSnapshot? = null
- loop@ while (eventType != END_TAG) {
- when (nextTag()) {
- START_TAG -> {
- bundleTagSnapshot = createTagSnapshot()
- }
- END_TAG -> {
- break@loop
- }
- }
- }
-
- return if (bundleTagSnapshot != null) {
- // swallow end tag
- nextTag()
- require(END_TAG, null, "attr")
-
- AaptAttrSnapshot(namespace, prefix, localName, id, bundleTagSnapshot)
- } else {
- null
- }
- }
-
- private fun findPrefixByQualifiedName(name: String): String {
- val prefixEnd = name.indexOf(':')
- return if (prefixEnd > 0) {
- name.substring(0, prefixEnd)
- } else ""
- }
-
- private fun findLocalNameByQualifiedName(name: String): String {
- return name.substring(name.indexOf(':') + 1)
- }
-
- private fun createAttributesForTag(): MutableList<AttributeSnapshot> {
- return buildList {
- for (i in 0 until attributeCount) {
- add(
- AttributeSnapshot(
- getAttributeNamespace(i),
- getAttributePrefix(i),
- getAttributeName(i),
- getAttributeValue(i)
- )
- )
- }
- }.toMutableList()
- }
-
- companion object {
- private var uniqueId = 0L
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/TagSnapshot.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/TagSnapshot.kt
deleted file mode 100644
index 19cab20..0000000
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/internal/parsers/TagSnapshot.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2021 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi.internal.parsers
-
-/**
- * Derived from https://cs.android.com/android-studio/platform/tools/adt/idea/+/mirror-goog-studio-master-dev:android/src/com/android/tools/idea/rendering/parsers/TagSnapshot.java
- *
- * A snapshot of the state of an xml tag.
- *
- * Used by the rendering architecture to be able to hold a consistent view of
- * the layout files across a long rendering operation without holding read locks,
- * as well as to for example let the property sheet evaluate and paint the values
- * of properties as they were at the time of rendering, not as they are at the current
- * instant.
- */
-data class TagSnapshot(
- val name: String,
- val namespace: String,
- val prefix: String?,
- val attributes: List<AttributeSnapshot>,
- val children: List<TagSnapshot>,
- val hasDeclaredAaptAttrs: Boolean = false
-) {
- var next: TagSnapshot? = null
-
- @Suppress("unused") // Used for debugging
- fun printFormatted(): String {
- indent++
- val output = """
- |$name:
- |${pad(indent + 1)}attributes: ${print(attributes)}
- |${pad(indent + 1)}children: ${print(children)}
- """.trimMargin()
- indent--
- return output
- }
-
- private fun print(children: List<*>): String {
- if (children.isEmpty()) return children.toString()
-
- indent++
- val output = children.joinToString(
- prefix = "[\n${pad(indent + 1)}",
- separator = "\n${pad(indent + 1)}",
- postfix = "\n${pad(indent)}]"
- )
- indent--
- return output
- }
-
- private fun pad(length: Int): String = " ".repeat(length)
-
- companion object {
- var indent = -1
- }
-}
diff --git a/external/paparazzi/paparazzi/src/main/resources/index.html b/external/paparazzi/paparazzi/src/main/resources/index.html
deleted file mode 100644
index 327f8b0..0000000
--- a/external/paparazzi/paparazzi/src/main/resources/index.html
+++ /dev/null
@@ -1,91 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <style type="text/css">
- body {
- background: #212121;
- text-align: center;
- }
-
- .screen {
- margin: 0.2em;
- display: inline-block;
- position: relative;
- overflow: hidden;
- }
-
- .screen img, .screen video {
- width: 300px;
- }
-
- div.overlay__hovered {
- display: block;
- }
-
- .overlay {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- background: rgba(25, 0, 237, .68);
- color: #fff;
- opacity: 0;
- display: none;
- transition: 150ms;
-
- -webkit-animation-name: slideIn;
- -webkit-animation-duration: 0.4s;
- animation-name: slideIn;
- animation-duration: 0.4s;
- animation-fill-mode: forwards;
- }
-
- @-webkit-keyframes slideIn {
- from {bottom: -300px; opacity: 0}
- to {bottom: 0; opacity: 1}
- }
-
- @keyframes slideIn {
- from {bottom: -300px; opacity: 0}
- to {bottom: 0; opacity: 1}
- }
-
- .test__details {
- text-align: left;
- padding-left: 1em;
- }
-
- .test__details__name,
- .test__details__class,
- .test__details__package {
- line-height: 0.7em;
- }
-
- .test__details__timestamp {
- margin-top: 2em;
- }
-
- .test__details__selector {
- width: 16px;
- height: 16px;
- border-radius: 50%;
- display: inline-block;
- border: 2px solid #fff;
- margin: .1em;
- margin-bottom: 1.3em;
- margin-top: 1em;
- }
-
- .test__details__selector:hover {
- background-color: white;
- }
- </style>
-</head>
-
-<script src="index.js"></script>
-<script src="paparazzi.js"></script>
-
-<body onload="bootstrap()">
- <span id="rootContainer"/>
-</body>
-</html>
diff --git a/external/paparazzi/paparazzi/src/main/resources/paparazzi.js b/external/paparazzi/paparazzi/src/main/resources/paparazzi.js
deleted file mode 100644
index 5c12ab1..0000000
--- a/external/paparazzi/paparazzi/src/main/resources/paparazzi.js
+++ /dev/null
@@ -1,222 +0,0 @@
-window.runs = {};
-
-class Run {
- constructor(id, data) {
- this.id = id;
- // TODO(oldergod) which entries are required/optional?
- this.data = data;
- }
-}
-
-class Shot {
- constructor(name, test) {
- this.name = name;
- this.test = test;
-
- [, this.package, this.clazz, this.method] = Shot.TestMethodRegex.exec(test);
-
- this.runs = [];
- }
-
- static get TestMethodRegex() {
- return /^(.*)\.(.*)#(.*)$/;
- }
-
- addRun(runId, file, timestamp) {
- this.runs.push(
- {
- 'id': runId,
- 'file': file,
- 'timestamp': timestamp
- }
- );
-
- if (file.endsWith('.png')) {
- this.img.src = file;
- this.img.style.display = 'inline';
- this.video.style.display = 'none';
- } else {
- this.video.src = file;
- this.video.style.display = 'inline';
- this.img.style.display = 'none';
- }
- this.timestampP.innerText = timestamp;
-
- const circle = document.createElement('div');
- circle.classList.add('test__details__selector', `run-${runId}`);
- circle.onmouseover = function (e) {
- if (file.endsWith('.png')) {
- this.img.src = file;
- } else {
- this.video.src = file;
- }
-
- for (let shot of Object.values(paparazziRenderer.shots)) {
- let found = false;
- for (let run of shot.runs) {
- if (runId == run.id) {
- shot.img.src = run.file;
- shot.timestampP.innerText = run.timestamp;
-
- found = true;
- break;
- }
- }
- shot.img.style.opacity = found ? "1" : "0.3";
- }
- }.bind(this);
- this.overlayDiv.appendChild(circle);
- }
-
- removeRun(runId) {
- const index = this.runs.indexOf((run) => run.id == runId);
- if (index == -1) return;
-
- this.runs.splice(index, 1);
- }
-
- inflate() {
- const screenDiv = document.createElement('div');
- screenDiv.classList.add('screen');
-
- document.rootContainer.appendChild(screenDiv);
-
- const img = document.createElement('img');
- const video = document.createElement('video');
- video.autoplay = 'autoplay';
- video.muted = 'muted';
- video.loop = 'loop';
-
- const overlayDiv = document.createElement('div');
- overlayDiv.classList.add('overlay');
-
- screenDiv.appendChild(img);
- screenDiv.appendChild(video);
- screenDiv.appendChild(overlayDiv);
- screenDiv.onmouseover = function (e) {
- overlayDiv.classList.add('overlay__hovered');
- }.bind(this);
- screenDiv.onmouseout = function (e) {
- overlayDiv.classList.remove('overlay__hovered');
- }.bind(this);
-
- const nameP = document.createElement('p');
- nameP.classList.add('test__details', 'test__details__name');
-
- const classP = document.createElement('p');
- classP.classList.add('test__details', 'test__details__class');
-
- const packageP = document.createElement('p');
- packageP.classList.add('test__details', 'test__details__package');
-
- const timestampP = document.createElement('p');
- timestampP.classList.add('test__details', 'test__details__timestamp');
-
- overlayDiv.appendChild(nameP);
- overlayDiv.appendChild(classP);
- overlayDiv.appendChild(packageP);
- overlayDiv.appendChild(timestampP);
-
- nameP.innerText = this.method;
- if (this.name !== undefined) {
- nameP.innerText += ` ${this.name}`;
- }
- classP.innerText = this.clazz;
- packageP.innerText = this.package;
-
- // hold references to the DOM for later updates
- this.img = img;
- this.video = video;
- this.timestampP = timestampP;
- this.overlayDiv = overlayDiv;
- }
-}
-
-class PaparazziRenderer {
- constructor() {
- // Used for content comparison for we only re-render the updated ones.
- this.currentRuns = {};
- // Used to store runs we know won't be updated anymore.
- this.lockedRunIds = [];
- this.shots = {}; // Key is `${test}${name}`, Value is a Shot.
- }
-
- start() {
- this.loadRunScript('index.js');
- for (let runId of window.all_runs) {
- this.loadRunScript(`runs/${runId}.js`);
- }
- setInterval(this.refresh.bind(this), 100);
- }
-
- render(run) {
- if (this.currentRuns[run.id]
- && JSON.stringify(this.currentRuns[run.id]) == JSON.stringify(run)) {
- // This run didn't change.
- return;
- }
- this.currentRuns[run.id] = run;
- console.log('rendering', run);
-
- for (let datum of run.data) {
- const key = `${datum.testName}${datum.name}`;
- let shot = this.shots[key];
- if (!shot) {
- console.log('New shot detected', shot);
- shot = new Shot(datum.name, datum.testName);
- this.shots[key] = shot;
- shot.inflate();
- } else {
- //shot.removeRun(run.id);
- }
-
- console.log('Adding run to shot', shot);
- shot.addRun(run.id, datum.file, datum.timestamp);
-
- // TODO setup listeners for filters/hovering, etc
- }
- }
-
- renderAll() {
- this.loadRunScript('index.js');
- for (let runId of window.all_runs) {
- if (this.lockedRunIds.includes(runId)) {
- continue;
- }
- // The js loading is async so the rendering can happen in the next refresh
- this.loadRunScript(`runs/${runId}.js`);
-
- this.render(new Run(runId, window.runs[runId]));
-
- const lastRunId = window.all_runs[window.all_runs.length - 1];
- if (runId != lastRunId) {
- // This run isn't the last run so we know it ain't gonna be updated.
- this.lockedRunIds.push(runId);
- delete this.currentRuns[runId];
- }
- }
- }
-
- refresh() {
- if (window.all_runs.length == 0) return;
-
- this.renderAll();
- }
-
- loadRunScript(js) {
- const script = document.createElement('script');
- script.src = js;
- script.onload = function () {
- this.remove();
- }
- document.head.appendChild(script);
- }
-}
-
-const paparazziRenderer = new PaparazziRenderer();
-console.log(paparazziRenderer);
-
-function bootstrap() {
- document.rootContainer = document.getElementById('rootContainer');
- paparazziRenderer.start();
-}
diff --git a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt b/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt
deleted file mode 100644
index 80979c7..0000000
--- a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/HtmlReportWriterTest.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright (C) 2019 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi
-
-import java.awt.image.BufferedImage
-import java.io.File
-import java.nio.file.Files
-import java.nio.file.Path
-import java.nio.file.attribute.BasicFileAttributes
-import java.nio.file.attribute.FileTime
-import java.time.Instant
-import java.util.Date
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.rules.TemporaryFolder
-
-class HtmlReportWriterTest {
- @get:Rule
- val reportRoot: TemporaryFolder = TemporaryFolder()
-
- @get:Rule
- val snapshotRoot: TemporaryFolder = TemporaryFolder()
-
- private val anyImage = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)
- private val anyImageHash = "9069ca78e7450a285173431b3e52c5c25299e473"
-
- @Test
- fun happyPath() {
- val htmlReportWriter = HtmlReportWriter("run_one", reportRoot.root)
- htmlReportWriter.use {
- val frameHandler = htmlReportWriter.newFrameHandler(
- Snapshot(
- name = "loading",
- testName = TestName("app.cash.paparazzi", "CelebrityTest", "testSettings"),
- timestamp = Instant.parse("2019-03-20T10:27:43Z").toDate(),
- tags = listOf("redesign")
- ),
- 1,
- -1
- )
- frameHandler.use {
- frameHandler.handle(anyImage)
- }
- }
-
- assertThat(File("${reportRoot.root}/index.js")).hasContent(
- """
- |window.all_runs = [
- | "run_one"
- |];
- |
- """.trimMargin()
- )
-
- assertThat(File("${reportRoot.root}/runs/run_one.js")).hasContent(
- """
- |window.runs["run_one"] = [
- | {
- | "name": "loading",
- | "testName": "app.cash.paparazzi.CelebrityTest#testSettings",
- | "timestamp": "2019-03-20T10:27:43.000Z",
- | "tags": [
- | "redesign"
- | ],
- | "file": "images/$anyImageHash.png"
- | }
- |];
- |
- """.trimMargin()
- )
- }
-
- @Test
- fun sanitizeForFilename() {
- assertThat("0 Dollars".sanitizeForFilename()).isEqualTo("0_dollars")
- assertThat("`!#$%&*+=|\\'\"<>?/".sanitizeForFilename()).isEqualTo("_________________")
- assertThat("~@^()[]{}:;,.".sanitizeForFilename()).isEqualTo("~@^()[]{}:;,.")
- }
-
- @Test
- fun noSnapshotOnFailure() {
- val htmlReportWriter = HtmlReportWriter("run_one", reportRoot.root)
- htmlReportWriter.use {
- val frameHandler = htmlReportWriter.newFrameHandler(
- snapshot = Snapshot(
- name = "loading",
- testName = TestName("app.cash.paparazzi", "CelebrityTest", "testSettings"),
- timestamp = Instant.parse("2019-03-20T10:27:43Z").toDate()
- ),
- frameCount = 4,
- fps = -1
- )
- frameHandler.use {
- // intentionally empty, to simulate no content written on exception
- }
- }
-
- assertThat(File(reportRoot.root, "images")).isEmptyDirectory
- assertThat(File(reportRoot.root, "videos")).isEmptyDirectory
- }
-
- @Test
- fun alwaysOverwriteOnRecord() {
- // set record mode
- System.setProperty("paparazzi.test.record", "true")
-
- val htmlReportWriter = HtmlReportWriter("record_run", reportRoot.root, snapshotRoot.root)
- htmlReportWriter.use {
- val now = Instant.parse("2021-02-23T10:27:43Z")
- val snapshot = Snapshot(
- name = "test",
- testName = TestName("app.cash.paparazzi", "HomeView", "testSettings"),
- timestamp = now.toDate()
- )
- val file =
- File("${snapshotRoot.root}/images/app.cash.paparazzi_HomeView_testSettings_test.png")
- val golden = file.toPath()
-
- // precondition
- assertThat(golden).doesNotExist()
-
- // take 1
- val frameHandler1 = htmlReportWriter.newFrameHandler(
- snapshot = snapshot,
- frameCount = 1,
- fps = -1
- )
- frameHandler1.use { frameHandler1.handle(anyImage) }
- assertThat(golden).exists()
- val timeFirstWrite = golden.lastModifiedTime()
-
- // I know....but guarantees writes won't happen in same tick
- Thread.sleep(100)
-
- // take 2
- val frameHandler2 = htmlReportWriter.newFrameHandler(
- snapshot = snapshot.copy(timestamp = now.plusSeconds(1).toDate()),
- frameCount = 1,
- fps = -1
- )
- frameHandler2.use { frameHandler2.handle(anyImage) }
- assertThat(golden).exists()
- val timeOverwrite = golden.lastModifiedTime()
-
- // should always overwrite
- assertThat(timeOverwrite).isGreaterThan(timeFirstWrite)
- }
- }
-
- private fun Instant.toDate() = Date(toEpochMilli())
-
- private fun Path.lastModifiedTime(): FileTime {
- return Files.readAttributes(this, BasicFileAttributes::class.java).lastModifiedTime()
- }
-}
diff --git a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/PaparazziTest.kt b/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/PaparazziTest.kt
deleted file mode 100644
index 356a6c8..0000000
--- a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/PaparazziTest.kt
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2019 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi
-
-import android.animation.AnimationHandler
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.ValueAnimator
-import android.graphics.Canvas
-import android.graphics.Color
-import android.view.Choreographer
-import android.view.Choreographer.CALLBACK_ANIMATION
-import android.view.View
-import android.view.animation.LinearInterpolator
-import android.widget.Button
-import com.android.internal.lang.System_Delegate
-import java.util.concurrent.TimeUnit
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-
-class PaparazziTest {
- @get:Rule
- val paparazzi = Paparazzi()
-
- @Ignore("b/245941625")
- @Test
- fun drawCalls() {
- val log = mutableListOf<String>()
-
- val view = object : View(paparazzi.context) {
- override fun onDraw(canvas: Canvas) {
- log += "onDraw time=$time"
- }
- }
-
- paparazzi.snapshot(view)
-
- assertThat(log).containsExactly("onDraw time=0")
- }
-
- @Ignore("b/245941625")
- @Test
- fun resetsAnimationHandler() {
- assertThat(AnimationHandler.sAnimatorHandler.get()).isNull()
-
- // Why Button? Because it sets a StateListAnimator on window attach
- // See https://github.com/cashapp/paparazzi/pull/319
- paparazzi.snapshot(Button(paparazzi.context))
-
- assertThat(AnimationHandler.sAnimatorHandler.get()).isNull()
- }
-
- @Ignore("b/245941625")
- @Test
- fun animationEvents() {
- val log = mutableListOf<String>()
-
- val animator = ValueAnimator.ofFloat(0.0f, 1.0f)
- animator.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationStart(animation: Animator) {
- log += "onAnimationStart time=$time animationElapsed=${animator.animatedValue}"
- }
-
- override fun onAnimationEnd(animation: Animator) {
- log += "onAnimationEnd time=$time animationElapsed=${animator.animatedValue}"
- }
- })
-
- val view = object : View(paparazzi.context) {
- override fun onDraw(canvas: Canvas) {
- log += "onDraw time=$time animationElapsed=${animator.animatedValue}"
- }
- }
-
- animator.addUpdateListener {
- log += "onAnimationUpdate time=$time animationElapsed=${animator.animatedValue}"
-
- val colorComponent = it.animatedFraction
- view.setBackgroundColor(Color.argb(1f, colorComponent, colorComponent, colorComponent))
- }
-
- animator.startDelay = 2_000L
- animator.duration = 1_000L
- animator.interpolator = LinearInterpolator()
- animator.start()
-
- paparazzi.gif(view, start = 1_000L, end = 4_000L, fps = 4)
-
- assertThat(log).containsExactly(
- "onDraw time=1000 animationElapsed=0.0",
- "onAnimationStart time=2000 animationElapsed=0.0",
- "onAnimationUpdate time=2000 animationElapsed=0.0",
- "onDraw time=2000 animationElapsed=0.0",
- "onAnimationUpdate time=2250 animationElapsed=0.25",
- "onDraw time=2250 animationElapsed=0.25",
- "onAnimationUpdate time=2500 animationElapsed=0.5",
- "onDraw time=2500 animationElapsed=0.5",
- "onAnimationUpdate time=2750 animationElapsed=0.75",
- "onDraw time=2750 animationElapsed=0.75",
- "onAnimationUpdate time=3000 animationElapsed=1.0",
- "onAnimationEnd time=3000 animationElapsed=1.0",
- "onDraw time=3000 animationElapsed=1.0"
- )
- }
-
- @Test
- @Ignore
- fun frameCallbacksExecutedAfterLayout() {
- val log = mutableListOf<String>()
-
- val view = object : View(paparazzi.context) {
- override fun onAttachedToWindow() {
- super.onAttachedToWindow()
- Choreographer.getInstance()
- .postCallback(
- CALLBACK_ANIMATION,
- { log += "view width=$width height=$height" },
- false
- )
- }
- }
-
- paparazzi.snapshot(view)
-
- assertThat(log).containsExactly("view width=1080 height=1776")
- }
-
- @Ignore("b/245941625")
- @Test
- fun throwsRenderingExceptions() {
- val view = object : View(paparazzi.context) {
- override fun onAttachedToWindow() {
- throw Throwable("Oops")
- }
- }
-
- val thrown = try {
- paparazzi.snapshot(view)
- false
- } catch (exception: Throwable) {
- true
- }
-
- assertThat(thrown).isTrue
- }
-
- private val time: Long
- get() {
- return TimeUnit.NANOSECONDS.toMillis(System_Delegate.nanoTime() - Paparazzi.TIME_OFFSET_NANOS)
- }
-}
diff --git a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/R.kt b/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/R.kt
deleted file mode 100644
index 5827e33..0000000
--- a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/R.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (C) 2019 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi
-
-/** Simulate an empty R.java for this package. */
-class R
diff --git a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/RenderingModeTest.kt b/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/RenderingModeTest.kt
deleted file mode 100644
index d774099..0000000
--- a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/RenderingModeTest.kt
+++ /dev/null
@@ -1,166 +0,0 @@
-package app.cash.paparazzi
-
-import android.content.Context
-import android.graphics.Color
-import android.graphics.drawable.GradientDrawable
-import android.view.Gravity
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Button
-import android.widget.LinearLayout
-import android.widget.TextView
-import app.cash.paparazzi.internal.ImageUtils
-import com.android.ide.common.rendering.api.SessionParams
-import java.awt.image.BufferedImage
-import java.io.File
-import javax.imageio.ImageIO
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.Description
-
-class RenderingModeTest {
-
- @Ignore("b/245941625")
- @Test
- fun `shrinks to wrap view`() {
- Paparazzi(
- snapshotHandler = TestSnapshotVerifier(),
- deviceConfig = DeviceConfig.NEXUS_5.copy(
- softButtons = false
- ),
- renderingMode = SessionParams.RenderingMode.SHRINK
- ).runTest("shrinks to wrap view") {
- val view = buildView(
- context,
- ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
- )
- snapshot(view, "rendering-mode-shrink")
- }
- }
-
- @Ignore("b/245941625")
- @Test
- fun `renders full device with RenderingMode NORMAL`() {
- Paparazzi(
- snapshotHandler = TestSnapshotVerifier(true),
- deviceConfig = DeviceConfig.NEXUS_5.copy(
- softButtons = false
- ),
- renderingMode = SessionParams.RenderingMode.NORMAL
- ).runTest("renders full device with RenderingMode NORMAL") {
- val view = buildView(
- context,
- ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
- )
- snapshot(view, "rendering-mode-normal")
- }
- }
-
- private fun Paparazzi.runTest(name: String, body: Paparazzi.() -> Unit) {
- try {
- prepare(Description.createTestDescription(this@RenderingModeTest::class.java, name))
- body()
- } finally {
- close()
- }
- }
-
- private fun buildView(
- context: Context,
- rootLayoutParams: ViewGroup.LayoutParams? = ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT
- )
- ) =
- LinearLayout(context).apply {
- orientation = LinearLayout.VERTICAL
- rootLayoutParams?.let { layoutParams = it }
- addView(
- TextView(context).apply {
- id = 1
- text = "Text View Sample"
- }
- )
-
- addView(
- View(context).apply {
- id = 2
- layoutParams = LinearLayout.LayoutParams(100, 100)
- contentDescription = "Content Description Sample"
- }
- )
-
- addView(
- View(context).apply {
- id = 3
- layoutParams = LinearLayout.LayoutParams(100, 100).apply {
- setMarginsRelative(20, 20, 20, 20)
- }
- contentDescription = "Margin Sample"
- }
- )
-
- addView(
- View(context).apply {
- id = 4
- layoutParams = LinearLayout.LayoutParams(100, 100).apply {
- setMarginsRelative(20, 20, 20, 20)
- }
- foreground = GradientDrawable(
- GradientDrawable.Orientation.TL_BR,
- intArrayOf(Color.YELLOW, Color.BLUE)
- ).apply {
- shape = GradientDrawable.OVAL
- }
- contentDescription = "Foreground Drawable"
- }
- )
-
- addView(
- Button(context).apply {
- id = 5
- layoutParams = LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- ).apply {
- gravity = Gravity.CENTER
- }
- text = "Button Sample"
- }
- )
- }
-}
-
-private class TestSnapshotVerifier(val writeImages: Boolean = false) : SnapshotHandler {
- override fun newFrameHandler(
- snapshot: Snapshot,
- frameCount: Int,
- fps: Int
- ): SnapshotHandler.FrameHandler {
- return object : SnapshotHandler.FrameHandler {
- override fun handle(image: BufferedImage) {
- val expected = File("src/test/resources/${snapshot.name}.png")
- if (writeImages) {
- ImageIO.write(image, "png", expected)
- } else {
- ImageUtils.assertImageSimilar(
- relativePath = expected.path,
- image = image,
- goldenImage = ImageIO.read(expected),
- maxPercentDifferent = 0.1
- )
- }
- }
-
- override fun close() = Unit
- }
- }
-
- override fun close() = Unit
-}
diff --git a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtensionTest.kt b/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtensionTest.kt
deleted file mode 100644
index b9afe6e..0000000
--- a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/accessibility/AccessibilityRenderExtensionTest.kt
+++ /dev/null
@@ -1,152 +0,0 @@
-package app.cash.paparazzi.accessibility
-
-import android.content.Context
-import android.graphics.Color
-import android.graphics.drawable.GradientDrawable
-import android.graphics.drawable.GradientDrawable.OVAL
-import android.graphics.drawable.GradientDrawable.Orientation.TL_BR
-import android.view.Gravity
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Button
-import android.widget.LinearLayout
-import android.widget.TextView
-import app.cash.paparazzi.DeviceConfig
-import app.cash.paparazzi.Paparazzi
-import app.cash.paparazzi.Snapshot
-import app.cash.paparazzi.SnapshotHandler
-import app.cash.paparazzi.internal.ImageUtils
-import java.awt.image.BufferedImage
-import java.io.File
-import javax.imageio.ImageIO
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-
-class AccessibilityRenderExtensionTest {
- private val snapshotHandler = TestSnapshotVerifier()
-
- @get:Rule
- val paparazzi = Paparazzi(
- deviceConfig = DeviceConfig.NEXUS_5.copy(
- // Needed to render accessibility content next to main content.
- screenWidth = DeviceConfig.NEXUS_5.screenWidth * 2,
- softButtons = false
- ),
- snapshotHandler = snapshotHandler,
- renderExtensions = setOf(AccessibilityRenderExtension())
- )
-
- @Ignore("b/245941625")
- @Test
- fun `verify baseline`() {
- val view = buildView(paparazzi.context)
- paparazzi.snapshot(view, name = "accessibility")
- }
-
- @Ignore("b/245941625")
- @Test
- fun `test without layout params set`() {
- val view = buildView(paparazzi.context, null)
- paparazzi.snapshot(view, name = "without-layout-params")
- }
-
- @Ignore("b/245941625")
- @Test
- fun `verify changing view hierarchy order doesn't change accessibility colors`() {
- val view = buildView(paparazzi.context).apply {
- addView(
- View(context).apply { contentDescription = "Empty View" },
- 0,
- LinearLayout.LayoutParams(0, 0)
- )
- }
- paparazzi.snapshot(view, name = "accessibility-new-view")
- }
-
- private fun buildView(
- context: Context,
- rootLayoutParams: ViewGroup.LayoutParams? = ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT
- )
- ) =
- LinearLayout(context).apply {
- orientation = LinearLayout.VERTICAL
- rootLayoutParams?.let { layoutParams = it }
- addView(
- TextView(context).apply {
- id = 1
- text = "Text View Sample"
- }
- )
-
- addView(
- View(context).apply {
- id = 2
- layoutParams = LinearLayout.LayoutParams(100, 100)
- contentDescription = "Content Description Sample"
- }
- )
-
- addView(
- View(context).apply {
- id = 3
- layoutParams = LinearLayout.LayoutParams(100, 100).apply {
- setMarginsRelative(20, 20, 20, 20)
- }
- contentDescription = "Margin Sample"
- }
- )
-
- addView(
- View(context).apply {
- id = 4
- layoutParams = LinearLayout.LayoutParams(100, 100).apply {
- setMarginsRelative(20, 20, 20, 20)
- }
- foreground = GradientDrawable(TL_BR, intArrayOf(Color.YELLOW, Color.BLUE)).apply {
- shape = OVAL
- }
- contentDescription = "Foreground Drawable"
- }
- )
-
- addView(
- Button(context).apply {
- id = 5
- layoutParams = LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- ).apply {
- gravity = Gravity.CENTER
- }
- text = "Button Sample"
- }
- )
- }
-
- private class TestSnapshotVerifier : SnapshotHandler {
- override fun newFrameHandler(
- snapshot: Snapshot,
- frameCount: Int,
- fps: Int
- ): SnapshotHandler.FrameHandler {
- return object : SnapshotHandler.FrameHandler {
- override fun handle(image: BufferedImage) {
- val expected = File("src/test/resources/${snapshot.name}.png")
- ImageUtils.assertImageSimilar(
- relativePath = expected.path,
- image = image,
- goldenImage = ImageIO.read(expected),
- maxPercentDifferent = 0.1
- )
- }
-
- override fun close() = Unit
- }
- }
-
- override fun close() = Unit
- }
-}
diff --git a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziJsonTest.kt b/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziJsonTest.kt
deleted file mode 100644
index 9752037..0000000
--- a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziJsonTest.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2019 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi.internal
-
-import app.cash.paparazzi.TestName
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Test
-
-class PaparazziJsonTest {
- @Test
- fun testName() {
- val adapter = PaparazziJson.moshi.adapter(TestName::class.java)
- val testName = TestName("app.cash.paparazzi", "CelebrityTest", "testSettings")
- val json = "\"app.cash.paparazzi.CelebrityTest#testSettings\""
- assertThat(adapter.toJson(testName)).isEqualTo(json)
- assertThat(adapter.fromJson(json)).isEqualTo(testName)
- }
-}
diff --git a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziLoggerTest.kt b/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziLoggerTest.kt
deleted file mode 100644
index 89d7823..0000000
--- a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/PaparazziLoggerTest.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package app.cash.paparazzi.internal
-
-import app.cash.paparazzi.internal.PaparazziLogger.MultipleFailuresException
-import java.io.FileNotFoundException
-import org.assertj.core.api.Assertions.assertThat
-import org.assertj.core.api.Assertions.fail
-import org.junit.Test
-
-class PaparazziLoggerTest {
- @Test
- fun testNoErrors() {
- val logger = PaparazziLogger()
-
- try {
- logger.assertNoErrors()
- } catch (ignored: Exception) {
- fail("Did not expect exception to be thrown: $ignored")
- }
- }
-
- @Test
- fun testSingleError() {
- val logger = PaparazziLogger()
- logger.error(FileNotFoundException("error1"), null)
-
- try {
- logger.assertNoErrors()
- fail("Expected exception to be thrown")
- } catch (ignored: Exception) {
- assertThat(ignored).isInstanceOf(FileNotFoundException::class.java)
- }
- }
-
- @Test
- fun testMultipleErrors() {
- val logger = PaparazziLogger()
- logger.error(FileNotFoundException("error1"), null)
- logger.error("tag", null, IllegalStateException("error2"), null, null)
-
- try {
- logger.assertNoErrors()
- fail("Expected exceptions to be thrown")
- } catch (ignored: Exception) {
- assertThat(ignored).isInstanceOf(MultipleFailuresException::class.java)
- assertThat(ignored.message).contains("There were 2 errors:")
- assertThat(ignored.message).contains("java.io.FileNotFoundException: error1")
- assertThat(ignored.message).contains("java.lang.IllegalStateException: error2")
- }
- }
-}
diff --git a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/InMemoryParserTest.kt b/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/InMemoryParserTest.kt
deleted file mode 100644
index 9a032e3..0000000
--- a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/InMemoryParserTest.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-package app.cash.paparazzi.internal.parsers
-
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Test
-import org.xmlpull.v1.XmlPullParserException
-
-class InMemoryParserTest {
- @Test
- fun parse() {
- val root = parseResourceTree("plus_sign.xml")
- val parser = RealInMemoryParser(root)
-
- assertThat(parser.name).isNull() // START_DOCUMENT
- assertThat(parser.depth).isEqualTo(0)
-
- parser.next() // START_TAG = "vector"
-
- assertThat(parser.name).isEqualTo(VECTOR_TAG_NAME)
- assertThat(parser.depth).isEqualTo(1)
- assertThat(parser.attributeCount).isEqualTo(4)
-
- assertThat(parser.getAttributeName(0)).isEqualTo("height")
- assertThat(parser.getAttributeName(1)).isEqualTo("viewportHeight")
- assertThat(parser.getAttributeName(2)).isEqualTo("viewportWidth")
- assertThat(parser.getAttributeName(3)).isEqualTo("width")
-
- assertThat(parser.getAttributeNamespace(0)).isEqualTo(ANDROID_NAMESPACE)
- assertThat(parser.getAttributeNamespace(1)).isEqualTo(ANDROID_NAMESPACE)
- assertThat(parser.getAttributeNamespace(2)).isEqualTo(ANDROID_NAMESPACE)
- assertThat(parser.getAttributeNamespace(3)).isEqualTo(ANDROID_NAMESPACE)
-
- assertThat(parser.getAttributePrefix(0)).isEqualTo(ANDROID_PREFIX)
- assertThat(parser.getAttributePrefix(1)).isEqualTo(ANDROID_PREFIX)
- assertThat(parser.getAttributePrefix(2)).isEqualTo(ANDROID_PREFIX)
- assertThat(parser.getAttributePrefix(3)).isEqualTo(ANDROID_PREFIX)
-
- assertThat(parser.getAttributeValue(0)).isEqualTo("24dp")
- assertThat(parser.getAttributeValue(1)).isEqualTo("40")
- assertThat(parser.getAttributeValue(2)).isEqualTo("40")
- assertThat(parser.getAttributeValue(3)).isEqualTo("24dp")
-
- parser.next() // START_TAG = "path"
-
- assertThat(parser.name).isEqualTo(PATH_TAG_NAME)
- assertThat(parser.depth).isEqualTo(2)
- assertThat(parser.attributeCount).isEqualTo(2)
-
- assertThat(parser.getAttributeName(0)).isEqualTo(FILL_COLOR_ATTR_NAME)
- assertThat(parser.getAttributeName(1)).isEqualTo(PATH_DATA_ATTR_NAME)
-
- assertThat(parser.getAttributeNamespace(0)).isEqualTo(ANDROID_NAMESPACE)
- assertThat(parser.getAttributeNamespace(1)).isEqualTo(ANDROID_NAMESPACE)
-
- assertThat(parser.getAttributePrefix(0)).isEqualTo(ANDROID_PREFIX)
- assertThat(parser.getAttributePrefix(1)).isEqualTo(ANDROID_PREFIX)
-
- assertThat(parser.getAttributeValue(0)).isEqualTo("#999999")
- assertThat(parser.getAttributeValue(1)).isNotNull // pathData
-
- parser.next() // END_TAG = "path"
-
- assertThat(parser.name).isEqualTo(PATH_TAG_NAME)
- assertThat(parser.depth).isEqualTo(2)
-
- parser.next() // END_TAG = "vector"
-
- assertThat(parser.name).isEqualTo(VECTOR_TAG_NAME)
- assertThat(parser.depth).isEqualTo(1)
-
- parser.next() // END_DOCUMENT
- assertThat(parser.name).isNull() // START_DOCUMENT
- assertThat(parser.depth).isEqualTo(0)
-
- try {
- parser.next()
- } catch (expected: XmlPullParserException) {
- }
- }
-
- private fun parseResourceTree(resourceId: String): TagSnapshot {
- val resourceInputStream = javaClass.classLoader.getResourceAsStream(resourceId)!!
- return ResourceParser(resourceInputStream).createTagSnapshot()
- }
-
- class RealInMemoryParser(private val root: TagSnapshot) : InMemoryParser() {
- override fun rootTag(): TagSnapshot = root
- }
-
- companion object {
- const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android"
- const val ANDROID_PREFIX = "android"
-
- const val VECTOR_TAG_NAME = "vector"
- const val PATH_TAG_NAME = "path"
- const val PATH_DATA_ATTR_NAME = "pathData"
- const val FILL_COLOR_ATTR_NAME = "fillColor"
- }
-}
diff --git a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/ResourceParserTest.kt b/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/ResourceParserTest.kt
deleted file mode 100644
index 67402f9..0000000
--- a/external/paparazzi/paparazzi/src/test/java/app/cash/paparazzi/internal/parsers/ResourceParserTest.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright (C) 2021 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.paparazzi.internal.parsers
-
-import org.assertj.core.api.Assertions.assertThat
-import org.junit.Test
-
-class ResourceParserTest {
- @Test
- fun parseResource() {
- val root = parseResourceTree("plus_sign.xml")
- assertThat(root.namespace).isEmpty()
- assertThat(root.prefix).isNull()
- assertThat(root.name).isEqualTo(VECTOR_TAG_NAME)
- assertThat(root.hasDeclaredAaptAttrs).isEqualTo(false)
- assertThat(root.next).isNull()
- assertThat(root.attributes).containsExactly(
- AttributeSnapshot(ANDROID_NAMESPACE, ANDROID_PREFIX, "height", "24dp"),
- AttributeSnapshot(ANDROID_NAMESPACE, ANDROID_PREFIX, "viewportHeight", "40"),
- AttributeSnapshot(ANDROID_NAMESPACE, ANDROID_PREFIX, "viewportWidth", "40"),
- AttributeSnapshot(ANDROID_NAMESPACE, ANDROID_PREFIX, "width", "24dp")
- )
-
- val pathElement = root.children.single()
- with(pathElement) {
- assertThat(namespace).isEmpty()
- assertThat(prefix).isNull()
- assertThat(name).isEqualTo(PATH_TAG_NAME)
- assertThat(hasDeclaredAaptAttrs).isEqualTo(false)
- assertThat(next).isNull()
- }
-
- val pathAttributes = pathElement.attributes
- assertThat(pathAttributes).hasSize(2)
- assertThat(pathAttributes[0]).isEqualTo(
- AttributeSnapshot(ANDROID_NAMESPACE, ANDROID_PREFIX, FILL_COLOR_ATTR_NAME, "#999999")
- )
-
- with(pathAttributes[1]) {
- assertThat(namespace).isEqualTo(ANDROID_NAMESPACE)
- assertThat(prefix).isEqualTo(ANDROID_PREFIX)
- assertThat(name).isEqualTo(PATH_DATA_ATTR_NAME)
- assertThat(value).isNotEmpty // don't care about pathData precision
- }
- }
-
- @Test
- fun parseAaptAttrTags() {
- // Since #parseResource covers the basics, this test can be more targeted
-
- val root = parseResourceTree("card_chip.xml")
- assertThat(root.name).isEqualTo(VECTOR_TAG_NAME)
- assertThat(root.hasDeclaredAaptAttrs).isEqualTo(true)
- assertThat(root.next).isNull()
-
- assertThat(root.children).hasSize(2)
-
- val outerPathElement = root.children[0]
- assertThat(outerPathElement.hasDeclaredAaptAttrs).isEqualTo(false)
-
- val groupElement = root.children[1]
- assertThat(groupElement.hasDeclaredAaptAttrs).isEqualTo(true)
-
- assertThat(outerPathElement.next).isEqualTo(groupElement)
-
- val clipPathElement = groupElement.children[0]
- assertThat(clipPathElement.hasDeclaredAaptAttrs).isEqualTo(false)
-
- val innerPathElement1 = groupElement.children[1]
- assertThat(innerPathElement1.hasDeclaredAaptAttrs).isEqualTo(true)
- with(innerPathElement1.attributes[0]) {
- assertThat(name).isEqualTo(PATH_DATA_ATTR_NAME)
- assertThat(this).isNotInstanceOf(AaptAttrSnapshot::class.java)
- }
- with(innerPathElement1.attributes[1] as AaptAttrSnapshot) {
- assertThat(name).isEqualTo(FILL_COLOR_ATTR_NAME)
- assertThat(id).isEqualTo("1")
- assertThat(value).isEqualTo("@aapt:_aapt/aapt1")
- assertThat(bundledTag.name).isEqualTo(GRADIENT_TAG_NAME) // 🎉
- }
-
- val innerPathElement2 = groupElement.children[2]
- assertThat(innerPathElement2.hasDeclaredAaptAttrs).isEqualTo(true)
- with(innerPathElement2.attributes[0]) {
- assertThat(name).isEqualTo(PATH_DATA_ATTR_NAME)
- assertThat(this).isNotInstanceOf(AaptAttrSnapshot::class.java)
- }
- with(innerPathElement2.attributes[1] as AaptAttrSnapshot) {
- assertThat(name).isEqualTo(FILL_COLOR_ATTR_NAME)
- assertThat(id).isEqualTo("2")
- assertThat(value).isEqualTo("@aapt:_aapt/aapt2")
- assertThat(bundledTag.name).isEqualTo(GRADIENT_TAG_NAME) // 🎉
- }
-
- val innerPathElement3 = groupElement.children[3]
- assertThat(innerPathElement3.hasDeclaredAaptAttrs).isEqualTo(true)
- with(innerPathElement3.attributes[0]) {
- assertThat(name).isEqualTo(PATH_DATA_ATTR_NAME)
- assertThat(this).isNotInstanceOf(AaptAttrSnapshot::class.java)
- }
- with(innerPathElement3.attributes[1]) {
- assertThat(name).isEqualTo(FILL_TYPE_ATTR_NAME)
- assertThat(this).isNotInstanceOf(AaptAttrSnapshot::class.java)
- }
- with(innerPathElement3.attributes[2] as AaptAttrSnapshot) {
- assertThat(name).isEqualTo(FILL_COLOR_ATTR_NAME)
- assertThat(id).isEqualTo("3")
- assertThat(value).isEqualTo("@aapt:_aapt/aapt3")
- assertThat(bundledTag.name).isEqualTo(GRADIENT_TAG_NAME) // 🎉
- }
- }
-
- private fun parseResourceTree(resourceId: String): TagSnapshot {
- val resourceInputStream = javaClass.classLoader.getResourceAsStream(resourceId)!!
- return ResourceParser(resourceInputStream).createTagSnapshot()
- }
-
- companion object {
- const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android"
- const val ANDROID_PREFIX = "android"
-
- const val VECTOR_TAG_NAME = "vector"
- const val PATH_TAG_NAME = "path"
- const val PATH_DATA_ATTR_NAME = "pathData"
- const val FILL_COLOR_ATTR_NAME = "fillColor"
- const val FILL_TYPE_ATTR_NAME = "fillType"
- const val GRADIENT_TAG_NAME = "gradient"
- }
-}
diff --git a/external/paparazzi/paparazzi/src/test/resources/accessibility-new-view.png b/external/paparazzi/paparazzi/src/test/resources/accessibility-new-view.png
deleted file mode 100644
index 57f25b5a..0000000
--- a/external/paparazzi/paparazzi/src/test/resources/accessibility-new-view.png
+++ /dev/null
Binary files differ
diff --git a/external/paparazzi/paparazzi/src/test/resources/accessibility.png b/external/paparazzi/paparazzi/src/test/resources/accessibility.png
deleted file mode 100644
index d7f3a16..0000000
--- a/external/paparazzi/paparazzi/src/test/resources/accessibility.png
+++ /dev/null
Binary files differ
diff --git a/external/paparazzi/paparazzi/src/test/resources/card_chip.xml b/external/paparazzi/paparazzi/src/test/resources/card_chip.xml
deleted file mode 100644
index a9cb398..0000000
--- a/external/paparazzi/paparazzi/src/test/resources/card_chip.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<!--Copyright Square, Inc.-->
-<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:width="42dp" android:height="34dp" android:viewportWidth="42" android:viewportHeight="34">
- <path android:pathData="M36.795 0H5.205C2.333 0 0 2.384 0 5.318v22.374c0 2.934 2.333 5.318 5.205 5.318h31.59c2.872 0 5.205-2.384 5.205-5.318V5.318C41.955 2.384 39.622 0 36.795 0z" android:fillColor="#666666"/>
- <group>
- <clip-path android:pathData="M0.314 21.549h11.981c0.404 0 0.808 0.183 1.122 0.504 1.48 1.559 3.5 2.338 6.058 2.338h0.224c2.378 0 3.814-0.825 3.814-2.109 0-1.192-1.211-1.742-4.711-3.072-3.097-1.146-7.18-3.163-7.45-7.748H0.27v0.459h0.404v9.17H0.27v0.458h0.045zm20.866 7.977c5.519-0.504 8.93-3.53 8.93-7.977 0-4.31-3.994-6.281-7.898-7.611-2.02-0.78-3.5-1.421-4.128-2.476h-6.058c0.27 4.172 4.083 6.052 7 7.107 3.32 1.237 5.115 1.97 5.115 3.713 0 1.742-1.705 2.797-4.487 2.797H19.43c-2.737 0-4.936-0.871-6.507-2.568-0.18-0.183-0.403-0.275-0.628-0.275H0.27v0.459h0.403v4.997c0 1.238 0.494 2.384 1.257 3.21L1.705 31.13l0.314 0.32 0.27-0.274c0.807 0.687 1.795 1.1 2.916 1.1h9.02l0.493-2.568c0.045-0.137-0.045-0.229-0.18-0.275-1.704-0.504-3.275-1.33-4.666-2.475-0.134-0.138-0.18-0.321-0.045-0.459 0.135-0.137 0.314-0.183 0.449-0.046 1.346 1.055 2.827 1.88 4.442 2.338 0.449 0.138 0.718 0.596 0.629 1.055l-0.45 2.43h5.026l0.45-2.063c0.089-0.367 0.403-0.642 0.807-0.688zM0.314 10.774h10.994c0.135-5.364 4.801-7.61 9.154-7.931 0.09-0.046 0.18-0.092 0.18-0.23L21 0.644H5.205c-1.121 0-2.109 0.412-2.916 1.1l-0.27-0.23L1.705 1.88 1.93 2.11C1.167 2.934 0.673 4.08 0.673 5.318v4.997H0.27v0.458h0.045zm41.327 11.417H30.738c-0.315 4.447-3.904 7.518-9.513 7.977-0.09 0-0.18 0.092-0.225 0.183l-0.404 1.926H36.75c1.122 0 2.11-0.413 2.917-1.1l0.27 0.275 0.313-0.321-0.224-0.23c0.808-0.825 1.301-1.97 1.301-3.209v-4.997h0.36L41.64 22.19zm-0.314-11.875V5.319c0-1.238-0.493-2.385-1.301-3.21L40.25 1.88l-0.314-0.367-0.224 0.23c-0.808-0.688-1.795-1.1-2.917-1.1h-9.333l-0.539 2.75c-0.044 0.092 0.045 0.23 0.135 0.275 1.212 0.458 2.378 1.1 3.455 1.834 0.135 0.092 0.18 0.32 0.09 0.458-0.045 0.092-0.18 0.138-0.27 0.138-0.044 0-0.134 0-0.179-0.046-1.032-0.688-2.109-1.284-3.276-1.696-0.403-0.138-0.673-0.596-0.583-1.009l0.539-2.613h-5.16l-0.36 2.017c-0.09 0.413-0.403 0.688-0.807 0.734-4.039 0.32-8.391 2.384-8.481 7.29h5.878c-0.045-0.138-0.045-0.276-0.045-0.413 0-1.467 1.436-2.384 3.904-2.384 1.795 0 4.577 0.642 6.641 2.476 0.224 0.183 0.494 0.32 0.763 0.32h12.564v-0.458h-0.404zm0 1.146H29.122c-0.404 0-0.852-0.183-1.211-0.458-1.93-1.697-4.533-2.339-6.193-2.339-0.538 0-3.23 0.092-3.23 1.697 0 1.33 1.57 2.017 3.948 2.934 4.128 1.421 8.302 3.484 8.302 8.253h10.948V21.09h-0.359v-9.17h0.36v-0.458h-0.36z"/>
- <path android:pathData="M0.314 21.549h11.981c0.404 0 0.808 0.183 1.122 0.504 1.48 1.559 3.5 2.338 6.058 2.338h0.224c2.378 0 3.814-0.825 3.814-2.109 0-1.192-1.211-1.742-4.711-3.072-3.097-1.146-7.18-3.163-7.45-7.748H0.27v0.459h0.404v9.17H0.27v0.458h0.045zm20.866 7.977c5.519-0.504 8.93-3.53 8.93-7.977 0-4.31-3.994-6.281-7.898-7.611-2.02-0.78-3.5-1.421-4.128-2.476h-6.058c0.27 4.172 4.083 6.052 7 7.107 3.32 1.237 5.115 1.97 5.115 3.713 0 1.742-1.705 2.797-4.487 2.797H19.43c-2.737 0-4.936-0.871-6.507-2.568-0.18-0.183-0.403-0.275-0.628-0.275H0.27v0.459h0.403v4.997c0 1.238 0.494 2.384 1.257 3.21L1.705 31.13l0.314 0.32 0.27-0.274c0.807 0.687 1.795 1.1 2.916 1.1h9.02l0.493-2.568c0.045-0.137-0.045-0.229-0.18-0.275-1.704-0.504-3.275-1.33-4.666-2.475-0.134-0.138-0.18-0.321-0.045-0.459 0.135-0.137 0.314-0.183 0.449-0.046 1.346 1.055 2.827 1.88 4.442 2.338 0.449 0.138 0.718 0.596 0.629 1.055l-0.45 2.43h5.026l0.45-2.063c0.089-0.367 0.403-0.642 0.807-0.688zM0.314 10.774h10.994c0.135-5.364 4.801-7.61 9.154-7.931 0.09-0.046 0.18-0.092 0.18-0.23L21 0.644H5.205c-1.121 0-2.109 0.412-2.916 1.1l-0.27-0.23L1.705 1.88 1.93 2.11C1.167 2.934 0.673 4.08 0.673 5.318v4.997H0.27v0.458h0.045zm41.327 11.417H30.738c-0.315 4.447-3.904 7.518-9.513 7.977-0.09 0-0.18 0.092-0.225 0.183l-0.404 1.926H36.75c1.122 0 2.11-0.413 2.917-1.1l0.27 0.275 0.313-0.321-0.224-0.23c0.808-0.825 1.301-1.97 1.301-3.209v-4.997h0.36L41.64 22.19zm-0.314-11.875V5.319c0-1.238-0.493-2.385-1.301-3.21L40.25 1.88l-0.314-0.367-0.224 0.23c-0.808-0.688-1.795-1.1-2.917-1.1h-9.333l-0.539 2.75c-0.044 0.092 0.045 0.23 0.135 0.275 1.212 0.458 2.378 1.1 3.455 1.834 0.135 0.092 0.18 0.32 0.09 0.458-0.045 0.092-0.18 0.138-0.27 0.138-0.044 0-0.134 0-0.179-0.046-1.032-0.688-2.109-1.284-3.276-1.696-0.403-0.138-0.673-0.596-0.583-1.009l0.539-2.613h-5.16l-0.36 2.017c-0.09 0.413-0.403 0.688-0.807 0.734-4.039 0.32-8.391 2.384-8.481 7.29h5.878c-0.045-0.138-0.045-0.276-0.045-0.413 0-1.467 1.436-2.384 3.904-2.384 1.795 0 4.577 0.642 6.641 2.476 0.224 0.183 0.494 0.32 0.763 0.32h12.564v-0.458h-0.404zm0 1.146H29.122c-0.404 0-0.852-0.183-1.211-0.458-1.93-1.697-4.533-2.339-6.193-2.339-0.538 0-3.23 0.092-3.23 1.697 0 1.33 1.57 2.017 3.948 2.934 4.128 1.421 8.302 3.484 8.302 8.253h10.948V21.09h-0.359v-9.17h0.36v-0.458h-0.36z">
- <aapt:attr name="android:fillColor">
- <gradient android:startY="3.4928" android:startX="1.38832" android:endY="46.3417" android:endX="68.7929" android:type="linear">
- <item android:offset="0" android:color="#FFFFFFFF"/>
- <item android:offset="0.4081" android:color="#FFC7B299"/>
- <item android:offset="1" android:color="#FF998675"/>
- </gradient>
- </aapt:attr>
- </path>
- <path android:pathData="M0.314 21.549h11.981c0.404 0 0.808 0.183 1.122 0.504 1.48 1.559 3.5 2.338 6.058 2.338h0.224c2.378 0 3.814-0.825 3.814-2.109 0-1.192-1.211-1.742-4.711-3.072-3.097-1.146-7.18-3.163-7.45-7.748H0.27v0.459h0.404v9.17H0.27v0.458h0.045zm20.866 7.977c5.519-0.504 8.93-3.53 8.93-7.977 0-4.31-3.994-6.281-7.898-7.611-2.02-0.78-3.5-1.421-4.128-2.476h-6.058c0.27 4.172 4.083 6.052 7 7.107 3.32 1.237 5.115 1.97 5.115 3.713 0 1.742-1.705 2.797-4.487 2.797H19.43c-2.737 0-4.936-0.871-6.507-2.568-0.18-0.183-0.403-0.275-0.628-0.275H0.27v0.459h0.403v4.997c0 1.238 0.494 2.384 1.257 3.21L1.705 31.13l0.314 0.32 0.27-0.274c0.807 0.687 1.795 1.1 2.916 1.1h9.02l0.493-2.568c0.045-0.137-0.045-0.229-0.18-0.275-1.704-0.504-3.275-1.33-4.666-2.475-0.134-0.138-0.18-0.321-0.045-0.459 0.135-0.137 0.314-0.183 0.449-0.046 1.346 1.055 2.827 1.88 4.442 2.338 0.449 0.138 0.718 0.596 0.629 1.055l-0.45 2.43h5.026l0.45-2.063c0.089-0.367 0.403-0.642 0.807-0.688zM0.314 10.774h10.994c0.135-5.364 4.801-7.61 9.154-7.931 0.09-0.046 0.18-0.092 0.18-0.23L21 0.644H5.205c-1.121 0-2.109 0.412-2.916 1.1l-0.27-0.23L1.705 1.88 1.93 2.11C1.167 2.934 0.673 4.08 0.673 5.318v4.997H0.27v0.458h0.045zm41.327 11.417H30.738c-0.315 4.447-3.904 7.518-9.513 7.977-0.09 0-0.18 0.092-0.225 0.183l-0.404 1.926H36.75c1.122 0 2.11-0.413 2.917-1.1l0.27 0.275 0.313-0.321-0.224-0.23c0.808-0.825 1.301-1.97 1.301-3.209v-4.997h0.36L41.64 22.19zm-0.314-11.875V5.319c0-1.238-0.493-2.385-1.301-3.21L40.25 1.88l-0.314-0.367-0.224 0.23c-0.808-0.688-1.795-1.1-2.917-1.1h-9.333l-0.539 2.75c-0.044 0.092 0.045 0.23 0.135 0.275 1.212 0.458 2.378 1.1 3.455 1.834 0.135 0.092 0.18 0.32 0.09 0.458-0.045 0.092-0.18 0.138-0.27 0.138-0.044 0-0.134 0-0.179-0.046-1.032-0.688-2.109-1.284-3.276-1.696-0.403-0.138-0.673-0.596-0.583-1.009l0.539-2.613h-5.16l-0.36 2.017c-0.09 0.413-0.403 0.688-0.807 0.734-4.039 0.32-8.391 2.384-8.481 7.29h5.878c-0.045-0.138-0.045-0.276-0.045-0.413 0-1.467 1.436-2.384 3.904-2.384 1.795 0 4.577 0.642 6.641 2.476 0.224 0.183 0.494 0.32 0.763 0.32h12.564v-0.458h-0.404zm0 1.146H29.122c-0.404 0-0.852-0.183-1.211-0.458-1.93-1.697-4.533-2.339-6.193-2.339-0.538 0-3.23 0.092-3.23 1.697 0 1.33 1.57 2.017 3.948 2.934 4.128 1.421 8.302 3.484 8.302 8.253h10.948V21.09h-0.359v-9.17h0.36v-0.458h-0.36z">
- <aapt:attr name="android:fillColor">
- <gradient android:startY="31.3798" android:startX="42.1542" android:endY="-9.12049" android:endX="13.8976" android:type="linear">
- <item android:offset="0" android:color="#FFF1EAE2"/>
- <item android:offset="0.149949" android:color="#FFF3ECE3"/>
- <item android:offset="0.319891" android:color="#FFF4EDE4"/>
- <item android:offset="0.509302" android:color="#FFEFE6DB"/>
- <item android:offset="0.707905" android:color="#FFECE4DB"/>
- <item android:offset="1" android:color="#FFF1F1F1"/>
- </gradient>
- </aapt:attr>
- </path>
- <path android:pathData="M1.25 0.5C1.08 0.775 1.166 1 1.441 1S2.08 0.775 2.25 0.5C2.42 0.225 2.334 0 2.059 0S1.42 0.225 1.25 0.5zM24 1.03c0 0.565 0.225 0.89 0.5 0.72 0.275-0.17 0.5-0.633 0.5-1.03C25 0.324 24.775 0 24.5 0S24 0.464 24 1.03zm4-0.78c0 0.138 0.113 0.25 0.25 0.25s0.25-0.112 0.25-0.25S28.387 0 28.25 0 28 0.113 28 0.25zm-8.181 2C19.661 2.663 19.749 3 20.016 3 20.282 3 20.5 2.663 20.5 2.25S20.412 1.5 20.303 1.5c-0.108 0-0.326 0.337-0.484 0.75zM24 2.75C24 2.888 24.113 3 24.25 3s0.25-0.112 0.25-0.25-0.113-0.25-0.25-0.25S24 2.612 24 2.75zm-4.76 1.78c-0.45 1.42 0.05 1.796 0.721 0.543 0.326-0.609 0.357-1.127 0.077-1.3-0.256-0.158-0.615 0.182-0.797 0.757zM22.83 4c-0.977 3.02-0.495 6.254 0.669 4.5 0.274-0.412 0.448-1.088 0.387-1.5-0.06-0.412 0.046-0.998 0.236-1.301 0.191-0.303 0.033-0.978-0.351-1.5C23.19 3.412 23.03 3.378 22.829 4zm2.895 1.875C24.304 9.252 23.56 14.5 24.5 14.5c0.275 0 0.5-0.575 0.5-1.278C25 11.64 26.04 7.34 26.504 7c0.188-0.138 0.493-0.869 0.678-1.625 0.501-2.046-0.536-1.69-1.457 0.5zM32 4.75C32 4.888 32.112 5 32.25 5s0.25-0.112 0.25-0.25-0.112-0.25-0.25-0.25S32 4.612 32 4.75zM0.508 5.875c0.004 0.206 0.217 0.706 0.473 1.11 0.576 0.911 1.251 0.341 0.846-0.714-0.298-0.779-1.333-1.09-1.32-0.396zM13 6.029c0 0.292 0.225 0.391 0.5 0.221S14 5.842 14 5.72c0-0.12-0.225-0.22-0.5-0.22S13 5.739 13 6.03zm20.54 0.173c-0.415 0.5-0.424 0.753-0.031 0.885C34.584 7.444 29.094 22 27.884 22c-0.223 0-1.383-0.9-2.576-2-3.16-2.913-3.467-2.449-0.421 0.636l2.603 2.635-1.295 1.398c-1.703 1.835-1.092 2.398 0.719 0.663 1.881-1.802 5.678-9.57 7.284-14.905C34.914 8.05 35.5 5.97 35.5 5.804c0-0.58-1.382-0.298-1.96 0.399zM2.876 6.33c0.344 0.139 0.906 0.139 1.25 0S4.188 6.079 3.5 6.079 2.531 6.193 2.875 6.331zM5 6.5C5 6.775 5.225 7 5.5 7S6 6.775 6 6.5 5.775 6 5.5 6 5 6.225 5 6.5zm10.5 0.25C15.5 6.888 15.613 7 15.75 7S16 6.888 16 6.75 15.887 6.5 15.75 6.5 15.5 6.612 15.5 6.75zM13 7.25c0 0.138 0.113 0.25 0.25 0.25s0.25-0.112 0.25-0.25S13.387 7 13.25 7 13 7.112 13 7.25zM8.5 8.146C6.987 8.373 5.02 8.447 4.128 8.311 3.098 8.155 2.245 8.327 1.79 8.783 1.395 9.177 0.83 9.5 0.535 9.5 0.241 9.5 0 10.063 0 10.75 0 11.943 0.136 12 2.966 12c2.761 0 3.044 0.11 4.096 1.587 0.656 0.922 1.216 1.329 1.336 0.971 0.112-0.338-0.191-1.053-0.674-1.587-0.484-0.534-0.788-0.983-0.676-0.998 0.111-0.015 1.954-0.21 4.096-0.436 2.142-0.224 3.995-0.51 4.12-0.634 0.124-0.124-0.05-0.81-0.387-1.524-0.723-1.532-2.324-1.841-6.377-1.233zM16 7.75C16 7.888 16.113 8 16.25 8s0.25-0.112 0.25-0.25-0.113-0.25-0.25-0.25S16 7.612 16 7.75zm12.278 2.778c-1.78 3.966-1.386 4.36 0.472 0.472 0.785-1.643 1.308-3.108 1.162-3.255-0.146-0.146-0.882 1.106-1.634 2.783zM13.334 8.834c-0.184 0.183-0.484 0.183-0.668 0C12.483 8.65 12.633 8.5 13 8.5s0.517 0.15 0.334 0.334zm4.968 0.541c-1.22 4.022-1.857 8.726-1.305 9.62 0.555 0.898 1.384-2.032 1.761-6.225 0.172-1.915 0.423-3.658 0.557-3.875 0.134-0.217 0.021-0.395-0.252-0.395-0.272 0-0.615 0.394-0.761 0.875zM4.5 9.25c0 0.137-0.112 0.25-0.25 0.25S4 9.387 4 9.25 4.112 9 4.25 9 4.5 9.113 4.5 9.25zm2 0c0 0.137-0.112 0.25-0.25 0.25S6 9.387 6 9.25 6.112 9 6.25 9 6.5 9.113 6.5 9.25zm4.375 0.108c-0.756 0.114-1.993 0.114-2.75 0C7.369 9.243 7.987 9.15 9.5 9.15c1.512 0 2.132 0.093 1.375 0.208zM22.08 11.93c-0.698 2.33-0.737 3.226-0.123 2.847 0.494-0.305 1.282-4.204 0.923-4.564-0.121-0.12-0.481 0.652-0.8 1.717zM3.5 13.5c0 0.367 0.15 0.517 0.333 0.334 0.183-0.184 0.183-0.484 0-0.668C3.65 12.983 3.5 13.133 3.5 13.5zm1-0.25c0 0.137 0.112 0.25 0.25 0.25S5 13.387 5 13.25 4.888 13 4.75 13 4.5 13.113 4.5 13.25zm28.334 2.584c-0.184 0.183-0.334 0.565-0.334 0.85 0 0.313 0.233 0.283 0.592-0.076 0.325-0.325 0.475-0.708 0.333-0.85-0.142-0.142-0.408-0.108-0.591 0.076zM15.5 16.25c0 0.137 0.113 0.25 0.25 0.25S16 16.387 16 16.25 15.887 16 15.75 16s-0.25 0.113-0.25 0.25zm3.959 2.375c-0.023 0.344-0.137 1.2-0.254 1.902-0.156 0.938 0.188 1.662 1.291 2.719 1.452 1.391 2.13 3.2 0.919 2.452-0.399-0.247-0.479-0.085-0.25 0.511 0.184 0.48 0.335 1.305 0.335 1.832 0 0.527 0.242 0.959 0.538 0.959 0.296 0 0.497-0.619 0.447-1.375-0.139-2.099-0.066-2.203 1.091-1.584 0.928 0.497 1.031 0.46 0.75-0.274C24.146 25.301 24 24.824 24 24.71c0-0.115-0.281-0.212-0.625-0.215-0.343-0.003-1.336-0.736-2.204-1.631-1.391-1.431-1.5-1.736-0.91-2.543 0.683-0.934 0.495-2.32-0.314-2.32-0.246 0-0.465 0.282-0.488 0.625zM6.75 21c-0.17 0.275-0.07 0.5 0.22 0.5 0.292 0 0.53-0.225 0.53-0.5s-0.099-0.5-0.22-0.5c-0.122 0-0.36 0.225-0.53 0.5zm9 0.09c-0.825 0.245-2.328 0.325-3.339 0.177-1.258-0.183-2.075-0.032-2.586 0.48-0.412 0.411-1.159 0.823-1.661 0.916C5.802 23.1 2.816 24.16 3.378 24.36c0.345 0.124 0.501 0.431 0.345 0.683C3.569 25.294 4.13 25.5 4.971 25.5 5.81 25.5 6.5 25.725 6.5 26s-0.956 0.503-2.125 0.508C2.819 26.514 2.45 26.645 3 27c0.413 0.267 1.699 0.488 2.86 0.492 1.7 0.006 2.05 0.158 1.81 0.788-0.165 0.428-0.6 0.663-0.968 0.521-0.369-0.14-0.803-0.041-0.965 0.222C5.574 29.285 5.932 29.5 6.532 29.5c0.856 0 0.97 0.145 0.534 0.671-0.715 0.861-0.279 1.291 0.977 0.963 0.696-0.183 0.898-0.534 0.688-1.196C8.528 29.304 8.676 29 9.186 29c0.415 0 0.892-0.223 1.06-0.494 0.175-0.284-0.026-0.367-0.471-0.197-1.216 0.467-0.908-0.593 0.349-1.199l1.125-0.543-1.125-0.033C8.85 26.496 8.62 25.753 9.75 25.319c0.412-0.158 0.75-0.621 0.75-1.03 0-0.658 2.703-1.778 4.31-1.786 0.585-0.003 1.464 4.422 1.266 6.372C16.014 29.494 16.197 30 16.482 30 16.791 30 17 28.576 17 26.47c0-2.225-0.184-3.415-0.5-3.22-0.786 0.485-0.585-0.328 0.33-1.338 1.061-1.173 0.792-1.377-1.08-0.822zm25.285 0.994c-0.019 0.459-0.171 1.19-0.338 1.625-0.2 0.519-0.028 0.791 0.5 0.791 0.875 0 0.97-0.698 0.303-2.25-0.343-0.799-0.437-0.832-0.465-0.166zM10.5 22.25c0 0.137-0.113 0.25-0.25 0.25S10 22.387 10 22.25 10.113 22 10.25 22s0.25 0.113 0.25 0.25zM39 23.5c0 0.366 0.15 0.517 0.334 0.334 0.182-0.184 0.182-0.484 0-0.668C39.15 22.983 39 23.134 39 23.5zM8.634 24.328c-0.338 0.137-1.013 0.146-1.5 0.018-0.486-0.127-0.209-0.238 0.616-0.248 0.825-0.01 1.223 0.093 0.884 0.23zM5 24.75C5 24.887 4.888 25 4.75 25S4.5 24.887 4.5 24.75s0.112-0.25 0.25-0.25S5 24.613 5 24.75zm26.5 0.75c0 0.275 0.238 0.5 0.53 0.5 0.29 0 0.39-0.225 0.22-0.5S31.842 25 31.72 25c-0.121 0-0.22 0.225-0.22 0.5zm6.822 0.54c-0.857 0.826-0.87 1.012-0.164 2.427C38.58 29.311 38.74 30 38.517 30c-0.223 0-0.375 0.394-0.337 0.875 0.048 0.605-0.355 0.924-1.305 1.033-0.86 0.099-1.375-0.079-1.375-0.475 0-0.465-0.148-0.485-0.556-0.076C33.995 32.303 34.96 33 37.225 33c1.465 0 2.26-0.243 2.455-0.75 0.158-0.413 0.547-0.75 0.864-0.75 0.416 0 0.404 0.21-0.045 0.75-0.52 0.627-0.448 0.75 0.439 0.75C41.717 33 42 32.689 42 31.834c0-1.512-0.486-1.968-1.341-1.258-0.852 0.706-1.284 0.331-0.867-0.755C39.965 29.37 40.532 29 41.053 29c1.19 0 1.244-0.85 0.072-1.155-0.834-0.218-0.834-0.232 0-0.287C42.063 27.496 42.429 25 41.5 25c-0.275 0-0.5 0.225-0.5 0.5 0 0.609-1.35 0.67-1.584 0.073-0.091-0.236-0.583-0.026-1.094 0.466zM8 25.72c0 0.122-0.225 0.36-0.5 0.53C7.225 26.42 7 26.32 7 26.03c0-0.292 0.225-0.53 0.5-0.53S8 25.599 8 25.72zm15 1.03c0 0.137 0.113 0.25 0.25 0.25s0.25-0.113 0.25-0.25-0.113-0.25-0.25-0.25S23 26.613 23 26.75zM1.5 27.5c0 0.366 0.15 0.517 0.333 0.334 0.184-0.184 0.184-0.484 0-0.668C1.65 26.983 1.5 27.134 1.5 27.5zm23.532 0.29c0.325 0.39 0.913 0.71 1.308 0.71 1.006 0 0.39-0.823-0.861-1.15-0.85-0.223-0.93-0.144-0.447 0.44zm-10.28 1.335c-0.18 0.894-0.429 1.992-0.554 2.441-0.126 0.448 0.06 0.926 0.412 1.06 0.465 0.178 0.667-0.492 0.736-2.44 0.11-3.047-0.114-3.446-0.594-1.061zm8.341 0.282c-0.078 1.37 0.023 1.65 0.393 1.093 0.603-0.907 0.664-2.349 0.114-2.688-0.22-0.136-0.448 0.582-0.507 1.595zM24.5 29.25c0 0.137 0.113 0.25 0.25 0.25S25 29.387 25 29.25 24.887 29 24.75 29s-0.25 0.113-0.25 0.25zm1.166 0.084c0.184 0.183 0.484 0.183 0.668 0C26.517 29.15 26.366 29 26 29s-0.517 0.15-0.334 0.334zM28 29.75c0 0.137 0.113 0.25 0.25 0.25s0.25-0.113 0.25-0.25-0.113-0.25-0.25-0.25S28 29.613 28 29.75zM1 30.5c0 0.366 0.15 0.517 0.333 0.334 0.183-0.184 0.183-0.484 0-0.668C1.15 29.983 1 30.134 1 30.5zm28-0.25c0 0.137 0.113 0.25 0.25 0.25s0.25-0.113 0.25-0.25S29.387 30 29.25 30 29 30.113 29 30.25zm-7.5 1.5c0 0.137 0.113 0.25 0.25 0.25S22 31.887 22 31.75s-0.113-0.25-0.25-0.25-0.25 0.113-0.25 0.25zm-14 0.75c0.982 0.635 2.5 0.635 2.5 0 0-0.275-0.731-0.496-1.625-0.492C7.142 32.013 6.931 32.132 7.5 32.5zm17.5 0c0.97 0.627 1.5 0.627 1.5 0 0-0.275-0.506-0.496-1.125-0.492-0.91 0.006-0.982 0.1-0.375 0.492z" android:fillType="evenOdd">
- <aapt:attr name="android:fillColor">
- <gradient android:startY="1.5" android:startX="32.5" android:endY="37.0292" android:endX="-0.969948" android:type="linear">
- <item android:offset="0" android:color="#FFECE3DA"/>
- <item android:offset="0.208782" android:color="#FFEBE2D6"/>
- <item android:offset="1" android:color="#FFEBE2D9"/>
- </gradient>
- </aapt:attr>
- </path>
- </group>
-</vector>
diff --git a/external/paparazzi/paparazzi/src/test/resources/plus_sign.xml b/external/paparazzi/paparazzi/src/test/resources/plus_sign.xml
deleted file mode 100644
index 15567605..0000000
--- a/external/paparazzi/paparazzi/src/test/resources/plus_sign.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="40" android:viewportWidth="40" android:width="24dp">
- <path android:fillColor="#999999" android:pathData="M21.5 14c0-0.828-0.672-1.5-1.5-1.5s-1.5 0.672-1.5 1.5v4.5H14c-0.828 0-1.5 0.672-1.5 1.5s0.672 1.5 1.5 1.5h4.5V26c0 0.828 0.672 1.5 1.5 1.5s1.5-0.672 1.5-1.5v-4.5H26c0.828 0 1.5-0.672 1.5-1.5s-0.672-1.5-1.5-1.5h-4.5V14z"/>
-</vector>
diff --git a/external/paparazzi/paparazzi/src/test/resources/rendering-mode-normal.png b/external/paparazzi/paparazzi/src/test/resources/rendering-mode-normal.png
deleted file mode 100644
index e69de29..0000000
--- a/external/paparazzi/paparazzi/src/test/resources/rendering-mode-normal.png
+++ /dev/null
diff --git a/external/paparazzi/paparazzi/src/test/resources/rendering-mode-shrink.png b/external/paparazzi/paparazzi/src/test/resources/rendering-mode-shrink.png
deleted file mode 100644
index e69de29..0000000
--- a/external/paparazzi/paparazzi/src/test/resources/rendering-mode-shrink.png
+++ /dev/null
diff --git a/external/paparazzi/paparazzi/src/test/resources/without-layout-params.png b/external/paparazzi/paparazzi/src/test/resources/without-layout-params.png
deleted file mode 100644
index e3da5cb..0000000
--- a/external/paparazzi/paparazzi/src/test/resources/without-layout-params.png
+++ /dev/null
Binary files differ
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b276acff..e35bc05 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -56,8 +56,6 @@
mockito = "2.25.0"
moshi = "1.13.0"
protobuf = "3.22.3"
-paparazzi = "1.0.0"
-paparazziNative = "2022.1.1-canary-f5f9f71"
skiko = "0.7.7"
spdxGradlePlugin = "0.3.0"
sqldelight = "1.3.0"
@@ -245,11 +243,6 @@
playServicesDevicePerformance = { module = "com.google.android.gms:play-services-deviceperformance", version = "16.0.0" }
playServicesFido = {module = "com.google.android.gms:play-services-fido", version = "20.1.0"}
playServicesWearable = { module = "com.google.android.gms:play-services-wearable", version = "17.1.0" }
-paparazzi = { module = "app.cash.paparazzi:paparazzi", version.ref = "paparazzi" }
-paparazziNativeJvm = { module = "app.cash.paparazzi:layoutlib-native-jdk11", version.ref = "paparazziNative" }
-paparazziNativeLinuxX64 = { module = "app.cash.paparazzi:layoutlib-native-linux", version.ref = "paparazziNative" }
-paparazziNativeMacOsArm64 = { module = "app.cash.paparazzi:layoutlib-native-macarm", version.ref = "paparazziNative" }
-paparazziNativeMacOsX64 = { module = "app.cash.paparazzi:layoutlib-native-macosx", version.ref = "paparazziNative" }
protobuf = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" }
protobufCompiler = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
protobufGradlePlugin = { module = "com.google.protobuf:protobuf-gradle-plugin", version = "0.9.4" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 4061606..82e12f6 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -238,7 +238,6 @@
<trusting group="com.google.auto.value"/>
<trusting group="com.google.auto.value" name="auto-value"/>
</trusted-key>
- <trusted-key id="70731C2FFB496DC4E8105AA3604F437C1682DDE5" group="app.cash.paparazzi"/>
<trusted-key id="70CD19BFD9F6C330027D6F260315BFB7970A144F">
<trusting group="com.sun.istack"/>
<trusting group="com.sun.xml.bind"/>
diff --git a/settings.gradle b/settings.gradle
index ac76bab..7c7abd3 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1113,7 +1113,6 @@
includeProject(":internal-testutils-macrobenchmark", "testutils/testutils-macrobenchmark", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":internal-testutils-navigation", "testutils/testutils-navigation", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
includeProject(":internal-testutils-paging", "testutils/testutils-paging", [BuildType.MAIN, BuildType.COMPOSE])
-includeProject(":internal-testutils-paparazzi", "testutils/testutils-paparazzi", [BuildType.COMPOSE])
includeProject(":internal-testutils-gradle-plugin", "testutils/testutils-gradle-plugin", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE, BuildType.TOOLS])
includeProject(":internal-testutils-mockito", "testutils/testutils-mockito", [BuildType.MAIN, BuildType.MEDIA, BuildType.FLAN, BuildType.COMPOSE])
includeProject(":kruth:kruth", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP, BuildType.COMPOSE])
@@ -1146,8 +1145,6 @@
includeProject(":external:libyuv", [BuildType.CAMERA])
includeProject(":noto-emoji-compat-font", new File(externalRoot, "noto-fonts/emoji-compat"), [BuildType.MAIN])
includeProject(":noto-emoji-compat-flatbuffers", new File(externalRoot, "noto-fonts/emoji-compat-flatbuffers"), [BuildType.MAIN, BuildType.COMPOSE])
-includeProject(":external:paparazzi:paparazzi", [BuildType.COMPOSE])
-includeProject(":external:paparazzi:paparazzi-agent", [BuildType.COMPOSE])
if (isAllProjects()) {
includeProject(":docs-tip-of-tree")
diff --git a/testutils/testutils-paparazzi/build.gradle b/testutils/testutils-paparazzi/build.gradle
deleted file mode 100644
index 68d62b0..0000000
--- a/testutils/testutils-paparazzi/build.gradle
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * This file was created using the `create_project.py` script located in the
- * `<AndroidX root>/development/project-creator` directory.
- *
- * Please use that script when creating a new project, rather than copying an existing project and
- * modifying its settings.
- */
-import androidx.build.BundleInsideHelper
-import androidx.build.LibraryType
-
-plugins {
- id("AndroidXPlugin")
- id("kotlin")
-}
-
-BundleInsideHelper.forInsideJar(
- project,
- "com.google.protobuf",
- // b/268288856 untangle this mess.
- // Currently this has to match test/screenshot/screenshot/build.gradle because sometimes
- // both :internal-testutils-paparazzi and :test:screenshot:screenshot are added to the
- // classpath and picking a different package will cause missing class exceptions due to
- // class shadowing
- "androidx.test.screenshot.protobuf",
- // proto-lite dependency includes .proto files, which are not used and would clash if
- // users also use proto library directly
- /* dropResourcesWithSuffix = */ ".proto"
-)
-
-dependencies {
- api(project(":external:paparazzi:paparazzi"))
- bundleInside(project(path: ":test:screenshot:screenshot-proto"))
-
- testImplementation(libs.junit)
- testImplementation(libs.kotlinTestJunit)
-}
-
-androidx {
- type = LibraryType.INTERNAL_HOST_TEST_LIBRARY
-}
\ No newline at end of file
diff --git a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/AndroidXPaparazzi.kt b/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/AndroidXPaparazzi.kt
deleted file mode 100644
index 510b7f2..0000000
--- a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/AndroidXPaparazzi.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.testutils.paparazzi
-
-import app.cash.paparazzi.DeviceConfig
-import app.cash.paparazzi.Environment
-import app.cash.paparazzi.Paparazzi
-import com.android.ide.common.rendering.api.SessionParams.RenderingMode
-import java.io.File
-
-/**
- * Creates a [Paparazzi] test rule configured from system properties for AndroidX tests with the
- * `AndroidXPaparazziPlugin` Gradle plugin applied.
- *
- * Golden images used with this framework are expected to have a one-to-one relationship to test
- * functions. This helps ensure isolation between test functions and facilitates updating golden
- * images programmatically via the `:updateGolden` Gradle task or CI.
- *
- * To this end, golden images are named by the qualified name of their test function, instead of
- * a secondary identifier. Additionally, the returned [Paparazzi] instance will enforce a limit of
- * one snapshot per test function.
- */
-fun androidxPaparazzi(
- deviceConfig: DeviceConfig = DeviceConfig.PIXEL_6.copy(softButtons = false),
- theme: String = "android:Theme.Material.NoActionBar.Fullscreen",
- renderingMode: RenderingMode = RenderingMode.SHRINK,
- imageDiffer: ImageDiffer = ImageDiffer.MSSIMMatcher
-) = Paparazzi(
- deviceConfig = deviceConfig,
- theme = theme,
- renderingMode = renderingMode,
- environment = Environment(
- platformDir = systemProperty("platformDir"),
- resDir = systemProperty("resDir"),
- assetsDir = systemProperty("assetsDir"),
- compileSdkVersion = systemProperty("compileSdkVersion").toInt(),
- resourcePackageNames = systemProperty("resourcePackageNames").split(","),
- appTestDir = System.getProperty("user.dir")!!
- ),
- snapshotHandler = GoldenVerifier(
- modulePath = systemProperty("modulePath"),
- goldenRootDirectory = File(systemProperty("goldenRootDir")),
- reportDirectory = File(systemProperty("reportDir")),
- imageDiffer = imageDiffer
- )
-)
-
-/** Package name used for resolving system properties */
-internal const val PACKAGE_NAME = "androidx.testutils.paparazzi"
-
-/** Name of the internal Gradle plugin */
-private const val PLUGIN_NAME = "AndroidXPaparazziPlugin"
-
-/** Name of the module containing this library */
-private const val MODULE_NAME = ":internal-testutils-paparazzi"
-
-/** Read a system property with [PACKAGE_NAME] prefix, throwing an exception if missing */
-private fun systemProperty(name: String) = checkNotNull(System.getProperty("$PACKAGE_NAME.$name")) {
- if (System.getProperty("$PACKAGE_NAME.gradlePluginApplied").toBoolean()) {
- "Missing required system property: $PACKAGE_NAME.$name. This is likely due to version " +
- "mismatch between $PLUGIN_NAME and $MODULE_NAME."
- } else {
- "Paparazzi system properties not set. Please ensure $PLUGIN_NAME is applied to your build."
- }
-}
diff --git a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/GoldenVerifier.kt b/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/GoldenVerifier.kt
deleted file mode 100644
index 8282bd9d..0000000
--- a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/GoldenVerifier.kt
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.testutils.paparazzi
-
-import androidx.test.screenshot.proto.ScreenshotResultProto.ScreenshotResult
-import androidx.test.screenshot.proto.ScreenshotResultProto.ScreenshotResult.Status
-import app.cash.paparazzi.Snapshot
-import app.cash.paparazzi.SnapshotHandler
-import app.cash.paparazzi.TestName
-import java.awt.image.BufferedImage
-import java.io.File
-import javax.imageio.ImageIO
-
-/**
- * This [SnapshotHandler] implements AndroidX-specific logic for screenshot testing. Specifically,
- * it writes a report to [reportDirectory] at every comparison just before throwing an exception,
- * even on successful comparison. The report contains text and binary protos containing the status
- * of the comparison and relative paths to actual, expected (copy of golden), and difference
- * (magenta highlights) images as applicable. CI consumes these reports to allow updating golden
- * images from the web UI. Golden images can also be updated by the `:updateGolden` Gradle task,
- * which copies actual images to their expected location in the golden repo.
- *
- * It also knows that the AndroidX golden repo, located at [goldenRootDirectory], is partitioned
- * by Gradle project path or [modulePath] and loads images from it.
- *
- * As noted in documentation on [androidxPaparazzi], the verifier is the component to enforce a
- * one-to-one relationship between test functions and golden images. This isn't strictly necessary,
- * but CI currently expects reports to be generated as a side effect of a regular test invocation.
- * Enforcing a one-to-one relationship avoids a situation where a failing assertion earlier in the
- * test would stop a later assertion from executing and generating its report as a side effect,
- * requiring multiple rounds of updating golden images to get the test passing.
- *
- * Additionally, CI currently only supports one comparison per report (note the [ScreenshotResult]
- * proto is not repeated). If this limit is lifted in the future, we would need to devise a scheme
- * for naming additional golden images such as an index of the order they were invoked and wrap the
- * test rule with something like an `ErrorCollector` to ensure later assertions are always
- * reachable.
- *
- * @property modulePath Unique path for the module, derived from Gradle project path. The verifier
- * will search for golden images in this directory relative to [goldenRootDirectory].
- *
- * @property goldenRootDirectory Location on disk of the golden images repo. Golden images for this
- * module are found in the [modulePath] directory under this directory.
- *
- * @property reportDirectory Directory to write reports, including protos, actual and expected
- * images, and image diffs.
- *
- * @property imageDiffer An [ImageDiffer] for comparing images.
- *
- * @property goldenRepoName Name of the repo containing golden images. Used for CI
- */
-internal class GoldenVerifier(
- val modulePath: String,
- val goldenRootDirectory: File,
- val reportDirectory: File,
- val imageDiffer: ImageDiffer = ImageDiffer.PixelPerfect,
- val goldenRepoName: String = ANDROIDX_GOLDEN_REPO_NAME
-) : SnapshotHandler {
- /** Directory containing golden images for this module. */
- val goldenDirectory = goldenRootDirectory.resolve(modulePath)
-
- /** The set of tests seen by this verifier, used to enforce single assertion per test. */
- private val attemptedTests = mutableSetOf<TestName>()
-
- /**
- * Asserts that the [actual] matches the expected golden for the test described in [snapshot].
- * As a side effect, this writes the report proto, actual, expected, and difference images to
- * [reportDirectory] as appropriate.
- */
- fun assertMatchesGolden(snapshot: Snapshot, actual: BufferedImage) {
- check(snapshot.testName !in attemptedTests) {
- "Snapshot already taken for test \"${snapshot.testName.methodName}\". Taking " +
- "multiple snapshots per test is not currently supported."
- }
- attemptedTests += snapshot.testName
-
- val expected = snapshot.toGoldenFile().takeIf { it.canRead() }?.let { ImageIO.read(it) }
- val analysis = analyze(expected, actual)
-
- fun updateMessage() = "To update golden images for this test module, run " +
- "${updateGoldenGradleCommand()}."
-
- writeReport(snapshot, analysis)
-
- when (analysis) {
- is AnalysisResult.Passed -> { /** Test passed, don't need to throw anything */ }
- is AnalysisResult.Failed -> throw AssertionError(
- "Actual image differs from golden image: ${analysis.imageDiff.description}. " +
- updateMessage()
- )
- is AnalysisResult.SizeMismatch -> throw AssertionError(
- "Actual image has different dimensions than golden image. " +
- "Actual: ${analysis.actual.width}x${analysis.actual.height}. " +
- "Golden: ${analysis.expected.width}x${analysis.expected.height}. " +
- updateMessage()
- )
- is AnalysisResult.MissingGolden -> throw AssertionError(
- "Expected golden image for test \"${snapshot.testName.methodName}\" does not " +
- "exist. Run ${updateGoldenGradleCommand()} " +
- "to create it and update all golden images for this test module."
- )
- }
- }
-
- /** Compare [expected] golden image to [actual] image and return an [AnalysisResult] */
- fun analyze(expected: BufferedImage?, actual: BufferedImage): AnalysisResult {
- if (expected == null) {
- return AnalysisResult.MissingGolden(actual)
- }
-
- if (actual.width != expected.width || actual.height != expected.height) {
- return AnalysisResult.SizeMismatch(actual, expected)
- }
-
- return when (val diff = imageDiffer.diff(actual, expected)) {
- is ImageDiffer.DiffResult.Similar -> AnalysisResult.Passed(actual, expected, diff)
- is ImageDiffer.DiffResult.Different -> AnalysisResult.Failed(actual, expected, diff)
- }
- }
-
- /**
- * Write the [analysis] for test described by [snapshot] to [reportDirectory] as both binary
- * and text proto, including actual, expected, and difference image files as appropriate.
- */
- fun writeReport(snapshot: Snapshot, analysis: AnalysisResult) {
- val actualFile = snapshot.toActualFile().also { ImageIO.write(analysis.actual, "PNG", it) }
- val goldenFile = snapshot.toGoldenFile()
-
- val resultProto = ScreenshotResult.newBuilder().apply {
- currentScreenshotFileName = actualFile.toRelativeString(reportDirectory)
- repoRootPath = goldenRepoName
- locationOfGoldenInRepo = goldenFile.toRelativeString(goldenRootDirectory)
- }
-
- fun diffFile(diff: BufferedImage) = snapshot.toDiffFile()
- .also { ImageIO.write(diff, "PNG", it) }
- .toRelativeString(reportDirectory)
-
- fun expectedFile() = goldenFile.copyTo(snapshot.toExpectedFile())
- .toRelativeString(reportDirectory)
-
- when (analysis) {
- is AnalysisResult.Passed -> resultProto.apply {
- result = Status.PASSED
- expectedImageFileName = expectedFile()
- analysis.imageDiff.highlights?.let { diffImageFileName = diffFile(it) }
- comparisonStatistics = analysis.imageDiff.taggedDescription()
- }
- is AnalysisResult.Failed -> resultProto.apply {
- result = Status.FAILED
- expectedImageFileName = expectedFile()
- diffImageFileName = diffFile(analysis.imageDiff.highlights)
- comparisonStatistics = analysis.imageDiff.taggedDescription()
- }
- is AnalysisResult.SizeMismatch -> resultProto.apply {
- result = Status.SIZE_MISMATCH
- expectedImageFileName = expectedFile()
- }
- is AnalysisResult.MissingGolden -> resultProto.apply {
- result = Status.MISSING_GOLDEN
- }
- }
-
- val result = resultProto.build()
- snapshot.toResultProtoFile().outputStream().use { result.writeTo(it) }
-
- // TODO(b/244200590): Remove text proto output, or replace with JSON
- snapshot.toResultTextProtoFile().writeText(result.toString())
- }
-
- /**
- * Analysis result ADT returned from [analyze], including actual and expected images and
- * an [ImageDiffer.DiffResult].
- */
- sealed interface AnalysisResult {
- val actual: BufferedImage
-
- data class Passed(
- override val actual: BufferedImage,
- val expected: BufferedImage,
- val imageDiff: ImageDiffer.DiffResult.Similar
- ) : AnalysisResult
-
- data class Failed(
- override val actual: BufferedImage,
- val expected: BufferedImage,
- val imageDiff: ImageDiffer.DiffResult.Different
- ) : AnalysisResult
-
- data class SizeMismatch(
- override val actual: BufferedImage,
- val expected: BufferedImage
- ) : AnalysisResult
-
- data class MissingGolden(
- override val actual: BufferedImage
- ) : AnalysisResult
- }
-
- override fun newFrameHandler(
- snapshot: Snapshot,
- frameCount: Int,
- fps: Int
- ): SnapshotHandler.FrameHandler {
- require(frameCount == 1) { "Videos are not yet supported" }
-
- return object : SnapshotHandler.FrameHandler {
- override fun handle(image: BufferedImage) {
- assertMatchesGolden(snapshot, image)
- }
-
- override fun close() = Unit
- }
- }
-
- override fun close() = Unit
-
- /** Adds [ImageDiffer.name] as a prefix to [ImageDiffer.DiffResult.description]. */
- private fun ImageDiffer.DiffResult.taggedDescription() =
- "[${imageDiffer.name}]: $description"
-
- // Filename templates based for a given snapshot
- private fun Snapshot.toGoldenFile() = goldenDirectory.resolve("${toFileName()}_paparazzi.png")
- private fun Snapshot.toExpectedFile() = reportDirectory.resolve("${toFileName()}_expected.png")
- private fun Snapshot.toActualFile() = reportDirectory.resolve("${toFileName()}_actual.png")
- private fun Snapshot.toDiffFile() = reportDirectory.resolve("${toFileName()}_diff.png")
- private fun Snapshot.toResultProtoFile() =
- reportDirectory.resolve("${toFileName()}_goldResult.pb")
- private fun Snapshot.toResultTextProtoFile() =
- reportDirectory.resolve("${toFileName()}_goldResult.textproto")
-
- companion object {
- /** Name of the AndroidX golden repo. */
- const val ANDROIDX_GOLDEN_REPO_NAME = "platform/frameworks/support-golden"
-
- /** Name of the updateGolden Gradle command for this module, via system properties. */
- fun updateGoldenGradleCommand() =
- "./gradlew ${
- (System.getProperty("$PACKAGE_NAME.updateGoldenTask") ?: ":updateGolden")
- } -Pandroidx.ignoreTestFailures=true"
-
- /** Render test function name as a fully qualified string. */
- fun TestName.toQualifiedName(): String {
- return if (packageName.isEmpty()) {
- "$className.$methodName"
- } else {
- "$packageName.$className.$methodName"
- }
- }
-
- /** Get a file name with special characters removed from a snapshot. */
- fun Snapshot.toFileName() = testName.toQualifiedName().replace(Regex("\\W+"), "_")
- }
-}
diff --git a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt b/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt
deleted file mode 100644
index 8902cb4..0000000
--- a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt
+++ /dev/null
@@ -1,369 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.testutils.paparazzi
-
-import androidx.testutils.paparazzi.ImageDiffer.DiffResult.Similar
-import java.awt.image.BufferedImage
-import kotlin.math.pow
-
-@JvmDefaultWithCompatibility
-/**
- * Functional interface to compare two images and returns a [ImageDiffer.DiffResult] ADT containing
- * comparison statistics and a difference image, if applicable.
- */
-fun interface ImageDiffer {
- /**
- * Compare image [a] to image [b]. Implementations may assume [a] and [b] have the same
- * dimensions.
- */
- fun diff(a: BufferedImage, b: BufferedImage): DiffResult
-
- /** A name to be used in logs for this differ, defaulting to the class's simple name. */
- val name
- get() = requireNotNull(this::class.simpleName) {
- "Could not determine ImageDiffer.name reflectively. Please override ImageDiffer.name."
- }
-
- /**
- * Result ADT returned from [diff].
- *
- * A differ may permit a small amount of difference, even for [Similar] results. Similar results
- * must include a [description], even if it's trivial, but may omit the [highlights] image if
- * it would be fully transparent.
- *
- * @property description A human-readable description of how the images differed, such as the
- * count of different pixels or percentage changed. Displayed in test failure messages and in
- * CI.
- *
- * @property highlights An image with a transparent background, highlighting where the compared
- * images differ, typically in shades of magenta. Displayed in CI.
- */
- sealed interface DiffResult {
- val description: String
- val highlights: BufferedImage?
-
- data class Similar(
- override val description: String,
- override val highlights: BufferedImage? = null
- ) : DiffResult
-
- data class Different(
- override val description: String,
- override val highlights: BufferedImage
- ) : DiffResult
- }
-
- /**
- * Pixel perfect image differ requiring images to be identical.
- *
- * The alpha channel is treated as pre-multiplied, meaning RGB channels may differ if the alpha
- * channel is 0 (fully transparent).
- */
- // TODO(b/244752233): Support wide gamut images.
- object PixelPerfect : ImageDiffer {
- override fun diff(a: BufferedImage, b: BufferedImage): DiffResult {
- check(a.width == b.width && a.height == b.height) { "Images are different sizes" }
- val width = a.width
- val height = b.height
- val highlights = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
- var count = 0
-
- for (x in 0 until width) {
- for (y in 0 until height) {
- val aPixel = a.getRGB(x, y)
- val bPixel = b.getRGB(x, y)
-
- // Compare full ARGB pixels, but allow other channels to differ if alpha is 0
- if (aPixel == bPixel || (aPixel ushr 24 == 0 && bPixel ushr 24 == 0)) {
- highlights.setRGB(x, y, TRANSPARENT.toInt())
- } else {
- count++
- highlights.setRGB(x, y, MAGENTA.toInt())
- }
- }
- }
-
- val description = "$count of ${width * height} pixels different"
- return if (count > 0) {
- DiffResult.Different(description, highlights)
- } else {
- DiffResult.Similar(description)
- }
- }
- }
-
- /**
- * Image comparison using Structural Similarity Index, developed by Wang, Bovik, Sheikh, and
- * Simoncelli. Details can be read in their paper:
- * https://ece.uwaterloo.ca/~z70wang/publications/ssim.pdf
- */
- object MSSIMMatcher : ImageDiffer {
- override fun diff(a: BufferedImage, b: BufferedImage): DiffResult {
- val aIntArray = a.toIntArray()
- val bIntArray = b.toIntArray()
- val SSIMTotal = calculateSSIM(aIntArray, bIntArray, a.width, a.height)
-
- val stats = "[MSSIM] Required SSIM: $SSIM_THRESHOLD, Actual " +
- "SSIM: " + "%.3f".format(SSIMTotal)
- if (SSIMTotal >= SSIM_THRESHOLD) {
- return DiffResult.Similar(stats)
- }
- return PixelPerfect.diff(a, b)
- }
-
- internal fun calculateSSIM(
- ideal: IntArray,
- given: IntArray,
- width: Int,
- height: Int
- ): Double {
- return calculateSSIM(ideal, given, 0, width, width, height)
- }
-
- private fun calculateSSIM(
- ideal: IntArray,
- given: IntArray,
- offset: Int,
- stride: Int,
- width: Int,
- height: Int
- ): Double {
- var SSIMTotal = 0.0
- var windows = 0
- var currentWindowY = 0
- while (currentWindowY < height) {
- val windowHeight = computeWindowSize(currentWindowY, height)
- var currentWindowX = 0
- while (currentWindowX < width) {
- val windowWidth = computeWindowSize(currentWindowX, width)
- val start: Int =
- indexFromXAndY(currentWindowX, currentWindowY, stride, offset)
- if (isWindowWhite(ideal, start, stride, windowWidth, windowHeight) &&
- isWindowWhite(given, start, stride, windowWidth, windowHeight)
- ) {
- currentWindowX += WINDOW_SIZE
- continue
- }
- windows++
- val means =
- getMeans(ideal, given, start, stride, windowWidth, windowHeight)
- val meanX = means[0]
- val meanY = means[1]
- val variances = getVariances(
- ideal, given, meanX, meanY, start, stride,
- windowWidth, windowHeight
- )
- val varX = variances[0]
- val varY = variances[1]
- val stdBoth = variances[2]
- val SSIM = SSIM(meanX, meanY, varX, varY, stdBoth)
- SSIMTotal += SSIM
- currentWindowX += WINDOW_SIZE
- }
- currentWindowY += WINDOW_SIZE
- }
- if (windows == 0) {
- return 1.0
- }
- return SSIMTotal / windows.toDouble()
- }
-
- /**
- * Compute the size of the window. The window defaults to WINDOW_SIZE, but
- * must be contained within dimension.
- */
- private fun computeWindowSize(coordinateStart: Int, dimension: Int): Int {
- return if (coordinateStart + WINDOW_SIZE <= dimension) {
- WINDOW_SIZE
- } else {
- dimension - coordinateStart
- }
- }
-
- private fun isWindowWhite(
- colors: IntArray,
- start: Int,
- stride: Int,
- windowWidth: Int,
- windowHeight: Int
- ): Boolean {
- for (y in 0 until windowHeight) {
- for (x in 0 until windowWidth) {
- if (colors[indexFromXAndY(x, y, stride, start)] != WHITE) {
- return false
- }
- }
- }
- return true
- }
-
- /**
- * This calculates the position in an array that would represent a bitmap given the parameters.
- */
- private fun indexFromXAndY(x: Int, y: Int, stride: Int, offset: Int): Int {
- return x + y * stride + offset
- }
-
- private fun SSIM(
- muX: Double,
- muY: Double,
- sigX: Double,
- sigY: Double,
- sigXY: Double
- ): Double {
- var SSIM = (2 * muX * muY + CONSTANT_C1) * (2 * sigXY + CONSTANT_C2)
- val denom = ((muX * muX + muY * muY + CONSTANT_C1) * (sigX + sigY + CONSTANT_C2))
- SSIM /= denom
- return SSIM
- }
-
- /**
- * This method will find the mean of a window in both sets of pixels. The return is an array
- * where the first double is the mean of the first set and the second double is the mean of the
- * second set.
- */
- private fun getMeans(
- pixels0: IntArray,
- pixels1: IntArray,
- start: Int,
- stride: Int,
- windowWidth: Int,
- windowHeight: Int
- ): DoubleArray {
- var avg0 = 0.0
- var avg1 = 0.0
- for (y in 0 until windowHeight) {
- for (x in 0 until windowWidth) {
- val index: Int = indexFromXAndY(x, y, stride, start)
- avg0 += getIntensity(pixels0[index])
- avg1 += getIntensity(pixels1[index])
- }
- }
- avg0 /= windowWidth * windowHeight.toDouble()
- avg1 /= windowWidth * windowHeight.toDouble()
- return doubleArrayOf(avg0, avg1)
- }
-
- /**
- * Finds the variance of the two sets of pixels, as well as the covariance of the windows. The
- * return value is an array of doubles, the first is the variance of the first set of pixels,
- * the second is the variance of the second set of pixels, and the third is the covariance.
- */
- private fun getVariances(
- pixels0: IntArray,
- pixels1: IntArray,
- mean0: Double,
- mean1: Double,
- start: Int,
- stride: Int,
- windowWidth: Int,
- windowHeight: Int
- ): DoubleArray {
- if (windowHeight == 1 && windowWidth == 1) {
- // There is only one item. The variance of a single item would be 0.
- // Since Bessel's correction is used below, it will return NaN instead of 0.
- return doubleArrayOf(0.0, 0.0, 0.0)
- }
-
- var var0 = 0.0
- var var1 = 0.0
- var varBoth = 0.0
- for (y in 0 until windowHeight) {
- for (x in 0 until windowWidth) {
- val index: Int = indexFromXAndY(x, y, stride, start)
- val v0 = getIntensity(pixels0[index]) - mean0
- val v1 = getIntensity(pixels1[index]) - mean1
- var0 += v0 * v0
- var1 += v1 * v1
- varBoth += v0 * v1
- }
- }
- // Using Bessel's correction. Hence, subtracting one.
- val denominatorWithBesselsCorrection = windowWidth * windowHeight - 1.0
- var0 /= denominatorWithBesselsCorrection
- var1 /= denominatorWithBesselsCorrection
- varBoth /= denominatorWithBesselsCorrection
- return doubleArrayOf(var0, var1, varBoth)
- }
-
- /**
- * Gets the intensity of a given pixel in RGB using luminosity formula
- *
- * l = 0.21R' + 0.72G' + 0.07B'
- *
- * The prime symbols dictate a gamma correction of 1.
- */
- private fun getIntensity(pixel: Int): Double {
- val gamma = 1.0
- var l = 0.0
- l += 0.21f * (red(pixel) / 255f.toDouble()).pow(gamma)
- l += 0.72f * (green(pixel) / 255f.toDouble()).pow(gamma)
- l += 0.07f * (blue(pixel) / 255f.toDouble()).pow(gamma)
- return l
- }
-
- /**
- * Return the red component of a color int. This is the same as saying
- * (color >> 16) & 0xFF
- */
- fun red(color: Int): Int {
- return color shr 16 and 0xFF
- }
-
- /**
- * Return the green component of a color int. This is the same as saying
- * (color >> 8) & 0xFF
- */
- fun green(color: Int): Int {
- return color shr 8 and 0xFF
- }
-
- /**
- * Return the blue component of a color int. This is the same as saying
- * color & 0xFF
- */
- fun blue(color: Int): Int {
- return color and 0xFF
- }
-
- private fun BufferedImage.toIntArray(): IntArray {
- val bitmapArray = IntArray(width * height)
- for (x in 0 until width) {
- for (y in 0 until height) {
- bitmapArray[y * width + x] = getRGB(x, y)
- }
- }
- return bitmapArray
- }
- }
-
- private companion object {
- const val MAGENTA = 0xFF_FF_00_FFu
- const val TRANSPARENT = 0x00_FF_FF_FFu
- const val WHITE = 0xFFFFFF
-
- // These values were taken from the publication
- private const val CONSTANT_L = 254.0
- private const val CONSTANT_K1 = 0.00001
- private const val CONSTANT_K2 = 0.00003
- private val CONSTANT_C1 = (CONSTANT_L * CONSTANT_K1).pow(2.0)
- private val CONSTANT_C2 = (CONSTANT_L * CONSTANT_K2).pow(2.0)
- private const val WINDOW_SIZE = 10
-
- private val SSIM_THRESHOLD: Double = 0.98
- }
-}
diff --git a/testutils/testutils-paparazzi/src/test/kotlin/androidx/testutils/paparazzi/GoldenVerifierTest.kt b/testutils/testutils-paparazzi/src/test/kotlin/androidx/testutils/paparazzi/GoldenVerifierTest.kt
deleted file mode 100644
index aee8a81..0000000
--- a/testutils/testutils-paparazzi/src/test/kotlin/androidx/testutils/paparazzi/GoldenVerifierTest.kt
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.testutils.paparazzi
-
-import androidx.test.screenshot.proto.ScreenshotResultProto.ScreenshotResult
-import androidx.test.screenshot.proto.ScreenshotResultProto.ScreenshotResult.Status
-import app.cash.paparazzi.Snapshot
-import java.awt.image.BufferedImage
-import java.io.File
-import java.util.Date
-import javax.imageio.ImageIO
-import kotlin.test.Test
-import kotlin.test.assertContains
-import kotlin.test.assertEquals
-import kotlin.test.assertFails
-import kotlin.test.assertFailsWith
-import kotlin.test.assertIs
-import org.junit.Rule
-import org.junit.rules.TemporaryFolder
-import org.junit.rules.TestName
-
-class GoldenVerifierTest {
- @get:Rule
- val testName = TestName()
-
- @get:Rule
- val goldenDirectory = TemporaryFolder()
-
- @get:Rule
- val reportDirectory = TemporaryFolder()
-
- private val modulePath = "testutils/testutils-paparazzi"
-
- @Test
- fun `snapshot handler success`() {
- createGolden("circle")
- goldenVerifier().newFrameHandler(snapshot(), 1, 0).handle(loadTestImage("circle"))
- }
-
- @Test
- fun `snapshot handler failure`() {
- createGolden("star")
- assertFails {
- goldenVerifier().newFrameHandler(snapshot(), 1, 0).handle(loadTestImage("circle"))
- }
- }
-
- @Test
- fun `removes special characters in file names`() {
- createGolden("circle")
- // Test that createGolden/goldenFile match naming in verifier
- goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle"))
-
- assertEquals(
- goldenFile().name,
- "androidx_testutils_paparazzi_GoldenVerifierTest_" +
- "removes_special_characters_in_file_names_paparazzi.png"
- )
- }
-
- @Test
- fun `writes report on success`() {
- createGolden("circle")
- goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle"))
-
- val proto = reportProto()
- assertEquals(Status.PASSED, proto.result)
- assertEquals(reportFile("expected.png").name, proto.expectedImageFileName)
- assertEquals("", proto.diffImageFileName)
- assertEquals("[PixelPerfect]: 0 of 65536 pixels different", proto.comparisonStatistics)
- assertContains(reportFile("goldResult.textproto").readText(), "PASSED")
- }
-
- @Test
- fun `writes actual image on success`() {
- createGolden("circle")
- goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle"))
- assertEquals(loadTestImage("circle"), reportFile("actual.png").readImage())
- }
-
- @Test
- fun `writes expected image on success`() {
- createGolden("circle")
- goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle"))
- assertEquals(loadTestImage("circle"), reportFile("expected.png").readImage())
- }
-
- @Test
- fun `analysis of success`() {
- val analysis = goldenVerifier().analyze(loadTestImage("circle"), loadTestImage("circle"))
- assertIs<GoldenVerifier.AnalysisResult.Passed>(analysis)
- assertEquals(loadTestImage("circle"), analysis.actual)
- assertEquals(loadTestImage("circle"), analysis.expected)
- }
-
- @Test
- fun `asserts on failure`() {
- createGolden("star")
- val message = "Actual image differs from golden image: 17837 of 65536 pixels different. " +
- "To update golden images for this test module, run ./gradlew :updateGolden " +
- "-Pandroidx.ignoreTestFailures=true."
-
- assertFailsWithMessage(message) {
- goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle"))
- }
- }
-
- @Test
- fun `writes result proto on failure`() {
- createGolden("star")
- assertFails { goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle")) }
-
- val proto = reportProto()
- assertEquals(Status.FAILED, proto.result)
- assertEquals(reportFile("expected.png").name, proto.expectedImageFileName)
- assertEquals(reportFile("diff.png").name, proto.diffImageFileName)
- assertEquals("[PixelPerfect]: 17837 of 65536 pixels different", proto.comparisonStatistics)
- assertContains(reportFile("goldResult.textproto").readText(), "FAILED")
- }
-
- @Test
- fun `writes actual image on failure`() {
- createGolden("star")
- assertFails { goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle")) }
- assertEquals(loadTestImage("circle"), reportFile("actual.png").readImage())
- }
-
- @Test
- fun `writes expected image on failure`() {
- createGolden("star")
- assertFails { goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle")) }
- assertEquals(loadTestImage("star"), reportFile("expected.png").readImage())
- }
-
- @Test
- fun `writes diff image on failure`() {
- createGolden("star")
- assertFails { goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle")) }
- assertEquals(loadTestImage("PixelPerfect_diff"), reportFile("diff.png").readImage())
- }
-
- @Test
- fun `analysis of failure`() {
- val analysis = goldenVerifier().analyze(loadTestImage("circle"), loadTestImage("star"))
- assertIs<GoldenVerifier.AnalysisResult.Failed>(analysis)
- assertEquals(loadTestImage("star"), analysis.actual)
- assertEquals(loadTestImage("circle"), analysis.expected)
- assertEquals(loadTestImage("PixelPerfect_diff"), analysis.imageDiff.highlights)
- }
-
- @Test
- fun `asserts on size mismatch`() {
- createGolden("horizontal_rectangle")
- val message = "Actual image has different dimensions than golden image. Actual: 72x128. " +
- "Golden: 128x72. To update golden images for this test module, run ./gradlew " +
- ":updateGolden -Pandroidx.ignoreTestFailures=true."
-
- assertFailsWithMessage(message) {
- goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("vertical_rectangle"))
- }
- }
-
- @Test
- fun `writes result proto for size mismatch`() {
- createGolden("horizontal_rectangle")
- assertFails {
- goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("vertical_rectangle"))
- }
-
- val proto = reportProto()
- assertEquals(Status.SIZE_MISMATCH, proto.result)
- assertEquals(reportFile("expected.png").name, proto.expectedImageFileName)
- assertEquals("", proto.diffImageFileName)
- assertEquals("", proto.comparisonStatistics)
- assertContains(reportFile("goldResult.textproto").readText(), "SIZE_MISMATCH")
- }
-
- @Test
- fun `writes actual image for size mismatch`() {
- createGolden("horizontal_rectangle")
- assertFails {
- goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("vertical_rectangle"))
- }
-
- assertEquals(loadTestImage("vertical_rectangle"), reportFile("actual.png").readImage())
- }
-
- @Test
- fun `writes expected image for size mismatch`() {
- createGolden("horizontal_rectangle")
- assertFails {
- goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("vertical_rectangle"))
- }
-
- assertEquals(loadTestImage("horizontal_rectangle"), reportFile("expected.png").readImage())
- }
-
- @Test
- fun `analysis of size mismatch`() {
- val analysis = goldenVerifier()
- .analyze(loadTestImage("horizontal_rectangle"), loadTestImage("vertical_rectangle"))
- assertIs<GoldenVerifier.AnalysisResult.SizeMismatch>(analysis)
- assertEquals(loadTestImage("vertical_rectangle"), analysis.actual)
- assertEquals(loadTestImage("horizontal_rectangle"), analysis.expected)
- }
-
- @Test
- fun `asserts on missing golden`() {
- val message = "Expected golden image for test \"asserts on missing golden\" does not " +
- "exist. Run ./gradlew :updateGolden -Pandroidx.ignoreTestFailures=true to create it " +
- "and update all golden images for this test module."
-
- assertFailsWithMessage(message) {
- goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle"))
- }
- }
-
- @Test
- fun `writes result proto for missing golden`() {
- assertFails { goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle")) }
-
- val proto = reportProto()
- assertEquals(Status.MISSING_GOLDEN, proto.result)
- assertEquals("", proto.expectedImageFileName)
- assertEquals("", proto.diffImageFileName)
- assertEquals("", proto.comparisonStatistics)
- assertContains(reportFile("goldResult.textproto").readText(), "MISSING_GOLDEN")
- }
-
- @Test
- fun `writes actual image for missing golden`() {
- assertFails { goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle")) }
- assertEquals(loadTestImage("circle"), reportFile("actual.png").readImage())
- }
-
- @Test
- fun `analysis of missing golden`() {
- val analysis = goldenVerifier().analyze(null, loadTestImage("circle"))
- assertIs<GoldenVerifier.AnalysisResult.MissingGolden>(analysis)
- assertEquals(loadTestImage("circle"), analysis.actual)
- }
-
- @Test
- fun `ensures single snapshot per method`() {
- val verifier = goldenVerifier()
- createGolden("circle")
-
- verifier.assertMatchesGolden(snapshot(), loadTestImage("circle"))
- assertFails { verifier.assertMatchesGolden(snapshot(), loadTestImage("circle")) }
- }
-
- private fun goldenVerifier() = GoldenVerifier(
- modulePath = modulePath,
- goldenRootDirectory = goldenDirectory.root,
- reportDirectory = reportDirectory.root
- )
-
- /** Assert [block] throws an [AssertionError] with supplied [message]. */
- private inline fun assertFailsWithMessage(message: String, block: () -> Unit) {
- assertEquals(message, assertFailsWith<AssertionError> { block() }.message)
- }
-
- /** Compare two images using [ImageDiffer.PixelPerfect]. */
- private fun assertEquals(expected: BufferedImage, actual: BufferedImage) {
- assertIs<ImageDiffer.DiffResult.Similar>(
- ImageDiffer.PixelPerfect.diff(expected, actual),
- message = "Expected images to be identical, but they were not."
- )
- }
-
- private fun snapshot() = Snapshot(
- name = null,
- testName = app.cash.paparazzi.TestName(
- packageName = this::class.java.packageName,
- className = this::class.simpleName!!,
- methodName = testName.methodName
- ),
- timestamp = Date()
- )
-
- /** Create a golden image for this test from the supplied test image [name]. */
- private fun createGolden(name: String) = javaClass.getResourceAsStream("$name.png")!!
- .copyTo(goldenFile().apply { parentFile!!.mkdirs() }.outputStream())
-
- /** Relative path to golden image for this test. */
- private fun goldenPath() = "$modulePath/${testName()}_paparazzi.png"
-
- /** Resolve the file path for a golden image for this test under [goldenDirectory]. */
- private fun goldenFile() = goldenDirectory.root.resolve(goldenPath()).canonicalFile
-
- /** Read the binary result proto under for this test and check common fields. */
- private fun reportProto() =
- ScreenshotResult.parseFrom(reportFile("goldResult.pb").inputStream()).also { proto ->
- assertEquals(reportFile("actual.png").name, proto.currentScreenshotFileName)
- assertEquals(GoldenVerifier.ANDROIDX_GOLDEN_REPO_NAME, proto.repoRootPath)
- assertEquals(goldenPath(), proto.locationOfGoldenInRepo)
- }
-
- /** Resolve the file path for a report file with provided [suffix] under [reportDirectory]. */
- private fun reportFile(suffix: String) =
- reportDirectory.root.resolve("${testName()}_$suffix").canonicalFile
-
- /** Convenience function to read an image from a file. */
- private fun File.readImage() = ImageIO.read(this)
-
- /** Fully qualified test ID with special characters replaced for this test. */
- private fun testName() = "${this::class.qualifiedName!!}_${testName.methodName}"
- .replace(Regex("\\W+"), "_")
-
- /** Load a test image from resources. */
- private fun loadTestImage(name: String) =
- ImageIO.read(javaClass.getResourceAsStream("$name.png")!!)
-}
diff --git a/testutils/testutils-paparazzi/src/test/kotlin/androidx/testutils/paparazzi/ImageDifferTest.kt b/testutils/testutils-paparazzi/src/test/kotlin/androidx/testutils/paparazzi/ImageDifferTest.kt
deleted file mode 100644
index 8b67607..0000000
--- a/testutils/testutils-paparazzi/src/test/kotlin/androidx/testutils/paparazzi/ImageDifferTest.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.testutils.paparazzi
-
-import androidx.testutils.paparazzi.ImageDiffer.DiffResult.Different
-import androidx.testutils.paparazzi.ImageDiffer.DiffResult.Similar
-import androidx.testutils.paparazzi.ImageDiffer.PixelPerfect
-import javax.imageio.ImageIO
-import kotlin.test.Test
-import kotlin.test.assertEquals
-import kotlin.test.assertIs
-import kotlin.test.assertNull
-
-class ImageDifferTest {
- @Test
- fun `PixelPerfect similar`() {
- val result = PixelPerfect.diff(loadTestImage("circle"), loadTestImage("circle"))
- assertIs<Similar>(result)
- assertEquals("0 of 65536 pixels different", result.description)
- assertNull(result.highlights)
- }
-
- @Test
- fun `PixelPerfect different`() {
- val result = PixelPerfect.diff(loadTestImage("circle"), loadTestImage("star"))
- assertIs<Different>(result)
- assertEquals("17837 of 65536 pixels different", result.description)
- assertIs<Similar>(
- PixelPerfect.diff(result.highlights, loadTestImage("PixelPerfect_diff"))
- )
- }
-
- @Test
- fun `PixelPerfect name`() {
- assertEquals("PixelPerfect", PixelPerfect.name)
- }
-
- private fun loadTestImage(name: String) =
- ImageIO.read(javaClass.getResourceAsStream("$name.png")!!)
-}
diff --git a/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/PixelPerfect_diff.png b/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/PixelPerfect_diff.png
deleted file mode 100644
index 8e3b7b8..0000000
--- a/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/PixelPerfect_diff.png
+++ /dev/null
Binary files differ
diff --git a/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/circle.png b/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/circle.png
deleted file mode 100644
index e6d58321..0000000
--- a/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/circle.png
+++ /dev/null
Binary files differ
diff --git a/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/horizontal_rectangle.png b/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/horizontal_rectangle.png
deleted file mode 100644
index a13221a..0000000
--- a/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/horizontal_rectangle.png
+++ /dev/null
Binary files differ
diff --git a/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/star.png b/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/star.png
deleted file mode 100644
index 61b0c64..0000000
--- a/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/star.png
+++ /dev/null
Binary files differ
diff --git a/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/vertical_rectangle.png b/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/vertical_rectangle.png
deleted file mode 100644
index 91a607e..0000000
--- a/testutils/testutils-paparazzi/src/test/resources/androidx/testutils/paparazzi/vertical_rectangle.png
+++ /dev/null
Binary files differ