blob: dee3137bffc03b136520a1dc21e6cc51144391f9 [file] [log] [blame]
/*
* Copyright 2019 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 android.app.Activity
import android.content.Context
import android.os.Bundle
import android.widget.LinearLayout
import java.net.URLClassLoader
import org.intellij.lang.annotations.Language
import org.jetbrains.kotlin.backend.common.output.OutputFile
import org.junit.Assert.assertEquals
import org.robolectric.Robolectric
fun printPublicApi(classDump: String, name: String): String {
return classDump
.splitToSequence("\n")
.filter {
if (it.contains("INVOKESTATIC kotlin/internal/ir/Intrinsic")) {
// if instructions like this end up in our generated code, it means something
// went wrong. Usually it means that it just can't find the function to call,
// so it transforms it into this intrinsic call instead of failing. If this
// happens, we want to hard-fail the test as the code is definitely incorrect.
error(
buildString {
append("An unresolved call was found in the generated bytecode of '")
append(name)
append("'")
appendLine()
appendLine()
appendLine("Call was: $it")
appendLine()
appendLine("Entire class file output:")
appendLine(classDump)
}
)
}
if (it.startsWith(" ")) {
if (it.startsWith(" ")) false
else it[2] != '/' && it[2] != '@'
} else {
it == "}" || it.endsWith("{")
}
}
.joinToString(separator = "\n")
.replace('$', '%') // replace $ to % to make comparing it to kotlin string literals easier
}
abstract class AbstractCodegenSignatureTest : AbstractCodegenTest(useFir = false) {
private fun OutputFile.printApi(): String {
return printPublicApi(asText(), relativePath)
}
protected fun checkApi(
@Language("kotlin") src: String,
expected: String,
dumpClasses: Boolean = false
) {
val className = "Test_REPLACEME_${uniqueNumber++}"
val fileName = "$className.kt"
val loader = classLoader(
"""
import androidx.compose.runtime.*
$src
""",
fileName, dumpClasses
)
val apiString = loader
.allGeneratedFiles
.filter { it.relativePath.endsWith(".class") }
.joinToString(separator = "\n") { it.printApi() }
.replace(className, "Test")
val expectedApiString = expected
.trimIndent()
.split("\n")
.filter { it.isNotBlank() }
.joinToString("\n")
assertEquals(expectedApiString, apiString)
}
protected fun checkComposerParam(
@Language("kotlin") src: String,
dumpClasses: Boolean = false
) {
val className = "Test_REPLACEME_${uniqueNumber++}"
val compiledClasses = classLoader(
"""
import androidx.compose.runtime.*
import android.widget.LinearLayout
import android.content.Context
import androidx.compose.ui.node.UiApplier
import androidx.compose.runtime.tooling.CompositionData
import androidx.compose.runtime.tooling.CompositionGroup
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
$src
class FakeApplier: Applier<Any> {
override val current: Any get() = this
override fun down(node: Any) { }
override fun up() { }
override fun insertTopDown(index: Int, instance: Any) { }
override fun insertBottomUp(index: Int, instance: Any) { }
override fun remove(index: Int, count: Int) { }
override fun move(from: Int, to: Int, count: Int) { }
override fun clear() { }
}
@OptIn(InternalComposeApi::class)
class FakeComposition: ControlledComposition {
override val isComposing: Boolean get() = false
override val isDisposed: Boolean get() = false
override val hasInvalidations: Boolean get() = false
override val hasPendingChanges: Boolean get() = false
override fun composeContent(content: () -> Unit) { }
override fun recordModificationsOf(values: Set<Any>) { }
override fun recordReadOf(value: Any) { }
override fun recordWriteOf(value: Any) { }
override fun recompose(): Boolean = false
override fun applyChanges() { }
override fun invalidateAll() { }
override fun verifyConsistent() { }
override fun dispose() { }
override fun setContent(content: () -> Unit) { }
}
@OptIn(InternalComposeApi::class)
class FakeComposer : Composer {
override val applier: Applier<*> = FakeApplier()
override val inserting: Boolean get() = true
override val skipping: Boolean get() = true
override val defaultsInvalid: Boolean get() = false
override val recomposeScope: RecomposeScope? get() = null
override val compoundKeyHash: Int get() = 0
override fun startReplaceableGroup(key: Int) { }
override fun startReplaceableGroup(key: Int, sourceInformation: String?) { }
override fun endReplaceableGroup() { }
override fun startMovableGroup(key: Int, dataKey: Any?) { }
override fun startMovableGroup(key: Int, dataKey: Any?, sourceInformation: String?) { }
override fun endMovableGroup() { }
override fun startDefaults() { }
override fun endDefaults() { }
override fun startRestartGroup(key: Int): Composer = this
override fun startRestartGroup(key: Int, sourceInformation: String?): Composer = this
override fun endRestartGroup(): ScopeUpdateScope? = null
override fun skipToGroupEnd() { }
override fun skipCurrentGroup() { }
override fun startNode() { }
override fun <T> createNode(factory: () -> T) { }
override fun useNode() { }
override fun endNode() { }
override fun <V, T> apply(value: V, block: T.(V) -> Unit) { }
override fun joinKey(left: Any?, right: Any?): Any = Any()
override fun rememberedValue(): Any? = Composer.Empty
override fun updateRememberedValue(value: Any?) { }
override fun changed(value: Any?): Boolean = true
override fun recordUsed(scope: RecomposeScope) { }
override fun recordSideEffect(effect: () -> Unit) { }
@Suppress("UNCHECKED_CAST")
override fun <T> consume(key: CompositionLocal<T>): T = null as T
override fun startProviders(values: Array<out ProvidedValue<*>>) { }
override fun endProviders() { }
override fun recordReadOf(value: Any) { }
override fun recordWriteOf(value: Any) { }
override val compositionData: CompositionData = object : CompositionData {
override val compositionGroups: Iterable<CompositionGroup> get() = emptyList()
override val isEmpty: Boolean get() = true
}
override fun collectParameterInformation() { }
override fun buildContext(): CompositionContext = error("Not mockable")
override val applyCoroutineContext: CoroutineContext get() = EmptyCoroutineContext
override val composition: ControlledComposition = FakeComposition()
}
@Composable fun assertComposer(expected: Composer?) {
val actual = currentComposer
assert(expected === actual)
}
fun makeComposer(): Composer = FakeComposer()
fun invokeComposable(composer: Composer?, fn: @Composable () -> Unit) {
if (composer == null) error("Composer was null")
val realFn = fn as Function2<Composer, Int, Unit>
realFn(composer, 1)
}
class Test {
fun test(context: Context) {
run()
}
}
""",
fileName = className,
dumpClasses = dumpClasses
)
val allClassFiles = compiledClasses.allGeneratedFiles.filter {
it.relativePath.endsWith(".class")
}
val loader = URLClassLoader(emptyArray(), this.javaClass.classLoader)
val instanceClass = run {
var instanceClass: Class<*>? = null
var loadedOne = false
for (outFile in allClassFiles) {
val bytes = outFile.asByteArray()
val loadedClass = loadClass(loader, null, bytes)
if (loadedClass.name == "Test") instanceClass = loadedClass
loadedOne = true
}
if (!loadedOne) error("No classes loaded")
instanceClass ?: error("Could not find class $className in loaded classes")
}
val instanceOfClass = instanceClass.getDeclaredConstructor().newInstance()
val testMethod = instanceClass.getMethod("test", Context::class.java)
val controller = Robolectric.buildActivity(TestActivity::class.java)
val activity = controller.create().get()
testMethod.invoke(instanceOfClass, activity)
}
private class TestActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(LinearLayout(this))
}
}
protected fun codegen(
@Language("kotlin") text: String,
dumpClasses: Boolean = false
) {
codegenNoImports(
"""
import android.content.Context
import android.widget.*
import androidx.compose.runtime.*
$text
fun used(x: Any?) {}
""",
dumpClasses
)
}
private fun codegenNoImports(
@Language("kotlin") text: String,
dumpClasses: Boolean = false
) {
val className = "Test_${uniqueNumber++}"
val fileName = "$className.kt"
classLoader(text, fileName, dumpClasses)
}
}