blob: 8ef8565c09573015dbee9a026af520e85e77dad7 [file] [log] [blame]
/*
* 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.compiler.plugins.kotlin
import java.io.File
import java.io.FileNotFoundException
import org.jetbrains.kotlin.incremental.createDirectory
import org.junit.Assert
import org.junit.rules.TestRule
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.junit.runners.model.Statement
private const val ENV_GENERATE_GOLDEN = "GENERATE_GOLDEN"
private const val GOLDEN_FILE_TYPE = "txt"
private fun env(name: String): Boolean = (System.getenv(name) ?: "false").toBoolean()
/**
* GoldenTransformRule
*
* Compare transformed IR source to a golden test file. Golden files contain both the
* pre-transformed and post-transformed source for easier review.
* To regenerate the set of golden tests, pass GENERATE_GOLDEN=true as an environment variable.
*
* @param pathToGoldens: Path to golden files
* @param generateGoldens: When true, will generate the golden test file and replace any existing
* @param generateMissingGoldens: When true, will generate a golden file for any that are not found.
**/
class GoldenTransformRule(
private val pathToGoldens: String,
private val generateGoldens: Boolean = env(ENV_GENERATE_GOLDEN),
private val generateMissingGoldens: Boolean = true
) : TestRule {
private lateinit var goldenFile: File
private lateinit var testIdentifier: String
private val testWatcher = object : TestWatcher() {
override fun starting(description: Description) {
val goldenFilePath = getGoldenFilePath(description.className, description.methodName)
goldenFile = File(goldenFilePath)
testIdentifier = "${description.className}_${description.methodName}"
}
}
private fun getGoldenFilePath(
className: String,
methodName: String
) = "$pathToGoldens/$className/$methodName.$GOLDEN_FILE_TYPE"
override fun apply(base: Statement, description: Description): Statement {
return base.run {
testWatcher.apply(this, description)
}
}
/**
* Verify the current test against the matching golden file.
* If generateGoldens is true, the golden file will first be generated.
*/
fun verifyGolden(testInfo: GoldenTransformTestInfo) {
if (generateGoldens || (!goldenFile.exists() && generateMissingGoldens)) {
saveGolden(testInfo)
}
if (!goldenFile.exists()) {
throw FileNotFoundException("Could not find golden file: ${goldenFile.absolutePath}")
}
val loadedTestInfo = try {
GoldenTransformTestInfo.fromEncodedString(goldenFile.readText())
} catch (e: IllegalStateException) {
error("Golden ${goldenFile.absolutePath} file could not be parsed.\n${e.message}")
}
// Use absolute path in the assert error so studio shows it as a link
Assert.assertEquals(
"Transformed source does not match golden file:" +
"\n${goldenFile.absolutePath}\n" +
"To regenerate golden files, pass GENERATE_GOLDEN=true as an env variable.",
loadedTestInfo.transformed,
testInfo.transformed
)
}
private fun saveGolden(testInfo: GoldenTransformTestInfo) {
val directory = goldenFile.parentFile!!
if (!directory.exists()) {
directory.createDirectory()
}
goldenFile.writeText(testInfo.encodeToString())
}
}
/**
* GoldenTransformTestInfo
* @param source The pre-transformed source code.
* @param transformed Post transformed IR tree source.
*/
data class GoldenTransformTestInfo(
val source: String,
val transformed: String
) {
fun encodeToString(): String =
buildString {
append(SOURCE_HEADER)
appendLine()
appendLine()
append(source)
appendLine()
appendLine()
append(TRANSFORM_HEADER)
appendLine()
appendLine()
append(transformed)
appendLine()
}
companion object {
val SOURCE_HEADER = """
//
// Source
// ------------------------------------------
""".trimIndent()
val TRANSFORM_HEADER = """
//
// Transformed IR
// ------------------------------------------
""".trimIndent()
fun fromEncodedString(encoded: String): GoldenTransformTestInfo {
val split = encoded.removePrefix(SOURCE_HEADER).split(TRANSFORM_HEADER)
if (split.size != 2) {
error("Could not parse encoded golden string. " +
"Expected 2 sections but was ${split.size}.")
}
return GoldenTransformTestInfo(split[0].trim(), split[1].trim())
}
}
}