blob: c1364fa07841d00421f18890983c2bd1d8293e26 [file] [log] [blame]
/*
* Copyright (C) 2024 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 com.android.tools.render.compose
import com.android.testutils.ImageDiffUtil
import com.android.testutils.TestUtils
import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TemporaryFolder
import java.io.File
import java.util.concurrent.TimeUnit
import javax.imageio.ImageIO
import kotlin.io.path.absolutePathString
import kotlin.io.path.readLines
class PerfMainTest {
private val tmpFolder = TemporaryFolder()
private val gradleProject = GradleProjectRule(
tmpFolder,
"tools/base/standalone-render/compose-cli/testData/compose-application",
"tools/external/gradle/gradle-8.2-bin.zip"
)
@JvmField
@Rule
val chain: RuleChain = RuleChain.outerRule(tmpFolder).around(gradleProject)
private fun createSettingsFile(outputFolder: File, resultsFile: File, screenshots: List<ComposeScreenshot>): File {
gradleProject.executeGradleTask(":app:assembleDebug")
gradleProject.executeGradleTask(":app:bundleDebugClassesToCompileJar")
gradleProject.executeGradleTask(":app:debugExtractClasspath", "--init-script", "initscript.gradle")
val apk = gradleProject.projectRoot.resolve("app/build/outputs/apk/debug/app-debug.apk")
val classPath = gradleProject.projectRoot.resolve("deps.txt").readLines()
val composeRendering = ComposeRendering(
TestUtils.getSdk().absolutePathString(),
TestUtils.resolveWorkspacePath("prebuilts/studio/layoutlib").absolutePathString(),
outputFolder.absolutePath,
classPath,
emptyList(),
"com.example.composeapplication",
apk.absolutePathString(),
screenshots,
resultsFile.absolutePath
)
val jsonSettings = tmpFolder.newFile()
writeComposeRenderingToJson(jsonSettings.bufferedWriter(), composeRendering)
return jsonSettings
}
@Test
fun testSingleSmall() {
val screenshots = listOf(
ComposeScreenshot(
"com.example.composeapplication.PreviewsKt.PreviewSmall",
emptyList(),
emptyMap(),
"screenshot"
)
)
commonTest(
screenshots,
"compose_cli_render_time_1_small",
"compose_cli_render_memory_1_small",
"small"
)
}
@Test
fun test10Small() {
val screenshots = (0..9).map {
ComposeScreenshot(
"com.example.composeapplication.PreviewsKt.PreviewSmall",
emptyList(),
emptyMap(),
"screenshot${it}"
)
}
commonTest(
screenshots,
"compose_cli_render_time_10_small",
"compose_cli_render_memory_10_small",
"small"
)
}
@Test
fun test40Small() {
val screenshots = (0..39).map {
ComposeScreenshot(
"com.example.composeapplication.PreviewsKt.PreviewSmall",
emptyList(),
emptyMap(),
"screenshot${it}"
)
}
commonTest(
screenshots,
"compose_cli_render_time_40_small",
"compose_cli_render_memory_40_small",
"small"
)
}
@Test
fun testSingleLarge() {
val screenshots = listOf(
ComposeScreenshot(
"com.example.composeapplication.PreviewsKt.PreviewLarge",
emptyList(),
emptyMap(),
"screenshot"
)
)
commonTest(
screenshots,
"compose_cli_render_time_1_large",
"compose_cli_render_memory_1_large",
"large"
)
}
@Test
fun test10Large() {
val screenshots = (0..9).map {
ComposeScreenshot(
"com.example.composeapplication.PreviewsKt.PreviewLarge",
emptyList(),
emptyMap(),
"screenshot${it}"
)
}
commonTest(
screenshots,
"compose_cli_render_time_10_large",
"compose_cli_render_memory_10_large",
"large"
)
}
@Test
fun test40Large() {
val screenshots = (0..39).map {
ComposeScreenshot(
"com.example.composeapplication.PreviewsKt.PreviewLarge",
emptyList(),
emptyMap(),
"screenshot${it}"
)
}
commonTest(
screenshots,
"compose_cli_render_time_40_large",
"compose_cli_render_memory_40_large",
"large"
)
}
private fun commonTest(
screenshots: List<ComposeScreenshot>,
timeMetricName: String,
memoryMetricName: String,
goldenName: String,
) {
val outputFolder = tmpFolder.newFolder()
val resultsFile = tmpFolder.newFile("results.json")
val jsonSettings = createSettingsFile(outputFolder, resultsFile, screenshots)
computeAndRecordMetric(timeMetricName, memoryMetricName) {
val metric = ComposeRenderingMetric()
metric.beforeTest()
runComposeCliRender(jsonSettings)
metric.afterTest()
val result = readComposeRenderingResultJson(resultsFile.bufferedReader())
assertNull(result.globalError)
result.screenshotResults.forEach {
assertNull(it.error)
}
screenshots.forEach { screenshot ->
val img = ImageIO.read(outputFolder.resolve("${screenshot.imageName}_0.png"))
ImageDiffUtil.assertImageSimilar(
TestUtils.resolveWorkspacePathUnchecked("tools/base/standalone-render/compose-cli/testData/goldens/$goldenName.png"),
img
)
}
metric
}
// Stop the daemon otherwise it could keep the lock on the temporary folder
gradleProject.executeGradleTask("--stop")
}
}
/** We want to filter out the error that we are aware of and that does not affect rendering. */
private val ALLOWED_ERRORS = listOf(
"WARNING: A terminally deprecated method in java.lang.System has been called",
"WARNING: System::setSecurityManager has been called by com.android.tools.rendering.security.RenderSecurityManager",
"WARNING: Please consider reporting this to the maintainers of com.android.tools.rendering.security.RenderSecurityManager",
"WARNING: System::setSecurityManager will be removed in a future release",
)
private fun runComposeCliRender(settingsFile: File): String {
val javaHome = System.getProperty("java.home")
val composeCliRenderFolder = TestUtils.resolveWorkspacePath("tools/base/standalone-render/compose-cli")
val command = listOf("$javaHome/bin/java", "-Dlayoutlib.thread.profile.timeoutms=10000", "-cp", "compose-preview-renderer.jar", "com.android.tools.render.compose.MainKt", settingsFile.absolutePath)
val procBuilder = ProcessBuilder(command)
.directory(composeCliRenderFolder.toFile())
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
// We have to specify JAVA_HOME
procBuilder.environment()["JAVA_HOME"] = javaHome
val proc = procBuilder.start()
proc.waitFor(5, TimeUnit.MINUTES)
val error = proc
.errorStream
.bufferedReader()
.readLines()
.filter { line -> ALLOWED_ERRORS.none { line.startsWith(it) } }
.joinToString("\n")
if (error.isNotEmpty()) {
val commandStr = command.joinToString(" ")
throw AssertionError("Error while rendering Compose previews \"$commandStr\":\n$error")
}
return proc.inputStream.bufferedReader().readText()
}