Add a test class validator to ravenizer
For now, just detect tests extending legacy base class, but just show
warnings.
Flag: EXEMPT host test change only
Bug: 292141694
Test: m $( frameworks/base/ravenwood/scripts/list-ravenwood-tests.sh )
and manually check the output
Change-Id: I689b97129c655f5feee3ce789477f493e995b853
diff --git a/ravenwood/scripts/remove-ravenizer-output.sh b/ravenwood/scripts/remove-ravenizer-output.sh
new file mode 100755
index 0000000..be15b71
--- /dev/null
+++ b/ravenwood/scripts/remove-ravenizer-output.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+# 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.
+
+# Delete all the ravenizer output jar files from Soong's intermediate directory.
+
+# `-a -prune` is needed because otherwise find would be confused if the directory disappears.
+
+find "${ANDROID_BUILD_TOP:?}/out/soong/.intermediates/" \
+ -type d \
+ -name 'ravenizer' \
+ -print \
+ -exec rm -fr \{\} \; \
+ -a -prune
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Exceptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Exceptions.kt
index 3a7fab3..0dcd271 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Exceptions.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Exceptions.kt
@@ -17,7 +17,14 @@
package com.android.platform.test.ravenwood.ravenizer
+import com.android.hoststubgen.UserErrorException
+
/**
* Use it for internal exception that really shouldn't happen.
*/
class RavenizerInternalException(message: String) : Exception(message)
+
+/**
+ * Thrown when an invalid test is detected in the target jar. (e.g. JUni3 tests)
+ */
+class RavenizerInvalidTestException(message: String) : Exception(message), UserErrorException
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
index e92ef72..a38512e 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
@@ -44,6 +44,9 @@
/** Time took to build [ClasNodes] */
var loadStructureTime: Double = .0,
+ /** Time took to validate the classes */
+ var validationTime: Double = .0,
+
/** Total real time spent for converting the jar file */
var totalProcessTime: Double = .0,
@@ -67,6 +70,7 @@
RavenizerStats{
totalTime=$totalTime,
loadStructureTime=$loadStructureTime,
+ validationTime=$validationTime,
totalProcessTime=$totalProcessTime,
totalConversionTime=$totalConversionTime,
totalCopyTime=$totalCopyTime,
@@ -84,16 +88,44 @@
class Ravenizer(val options: RavenizerOptions) {
fun run() {
val stats = RavenizerStats()
+
+ val fatalValidation = options.fatalValidation.get
+
stats.totalTime = log.nTime {
- process(options.inJar.get, options.outJar.get, stats)
+ process(
+ options.inJar.get,
+ options.outJar.get,
+ options.enableValidation.get,
+ fatalValidation,
+ stats,
+ )
}
log.i(stats.toString())
}
- private fun process(inJar: String, outJar: String, stats: RavenizerStats) {
+ private fun process(
+ inJar: String,
+ outJar: String,
+ enableValidation: Boolean,
+ fatalValidation: Boolean,
+ stats: RavenizerStats,
+ ) {
var allClasses = ClassNodes.loadClassStructures(inJar) {
time -> stats.loadStructureTime = time
}
+ if (enableValidation) {
+ stats.validationTime = log.iTime("Validating classes") {
+ if (!validateClasses(allClasses)) {
+ var message = "Invalid test class(es) detected." +
+ " See error log for details."
+ if (fatalValidation) {
+ throw RavenizerInvalidTestException(message)
+ } else {
+ log.w("Warning: $message")
+ }
+ }
+ }
+ }
stats.totalProcessTime = log.iTime("$executableName processing $inJar") {
ZipFile(inJar).use { inZip ->
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
index e85e3be..e8341e5 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
@@ -27,6 +27,12 @@
/** Output jar file */
var outJar: SetOnce<String> = SetOnce(""),
+
+ /** Whether to enable test validation. */
+ var enableValidation: SetOnce<Boolean> = SetOnce(true),
+
+ /** Whether the validation failure is fatal or not. */
+ var fatalValidation: SetOnce<Boolean> = SetOnce(false),
) {
companion object {
fun parseArgs(args: Array<String>): RavenizerOptions {
@@ -52,6 +58,12 @@
"--in-jar" -> ret.inJar.set(nextArg()).ensureFileExists()
"--out-jar" -> ret.outJar.set(nextArg())
+ "--enable-validation" -> ret.enableValidation.set(true)
+ "--disable-validation" -> ret.enableValidation.set(false)
+
+ "--fatal-validation" -> ret.fatalValidation.set(true)
+ "--no-fatal-validation" -> ret.fatalValidation.set(false)
+
else -> throw ArgumentsException("Unknown option: $arg")
}
} catch (e: SetOnce.SetMoreThanOnceException) {
@@ -74,6 +86,8 @@
RavenizerOptions{
inJar=$inJar,
outJar=$outJar,
+ enableValidation=$enableValidation,
+ fatalValidation=$fatalValidation,
}
""".trimIndent()
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
index e026e7a..059f5a4 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
@@ -87,9 +87,12 @@
return this.startsWithAny(
"java/", // just in case...
"javax/",
+ "junit/",
"org/junit/",
"org/mockito/",
"kotlin/",
+ "androidx/",
+ "android/support/",
// TODO -- anything else?
)
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
new file mode 100644
index 0000000..27092d2
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.platform.test.ravenwood.ravenizer
+
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.startsWithAny
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import com.android.hoststubgen.log
+import org.objectweb.asm.tree.ClassNode
+
+fun validateClasses(classes: ClassNodes): Boolean {
+ var allOk = true
+ classes.forEach { allOk = checkClass(it, classes) && allOk }
+
+ return allOk
+}
+
+/**
+ * Validate a class.
+ *
+ * - A test class shouldn't extend
+ *
+ */
+fun checkClass(cn: ClassNode, classes: ClassNodes): Boolean {
+ if (cn.name.shouldByBypassed()) {
+ // Class doesn't need to be checked.
+ return true
+ }
+ var allOk = true
+
+ // See if there's any class that extends a legacy base class.
+ // But ignore the base classes in android.test.
+ if (!cn.name.startsWithAny("android/test/")) {
+ allOk = checkSuperClass(cn, cn, classes) && allOk
+ }
+ return allOk
+}
+
+fun checkSuperClass(targetClass: ClassNode, currentClass: ClassNode, classes: ClassNodes): Boolean {
+ if (currentClass.superName == null || currentClass.superName == "java/lang/Object") {
+ return true // No parent class
+ }
+ if (currentClass.superName.isLegacyTestBaseClass()) {
+ log.e("Error: Class ${targetClass.name.toHumanReadableClassName()} extends"
+ + " a legacy test class ${currentClass.superName.toHumanReadableClassName()}.")
+ return false
+ }
+ classes.findClass(currentClass.superName)?.let {
+ return checkSuperClass(targetClass, it, classes)
+ }
+ // Super class not found.
+ // log.w("Class ${currentClass.superName} not found.")
+ return true
+}
+
+/**
+ * Check if a class internal name is a known legacy test base class.
+ */
+fun String.isLegacyTestBaseClass(): Boolean {
+ return this.startsWithAny(
+ "junit/framework/TestCase",
+
+ // In case the test doesn't statically include JUnit, we need
+ "android/test/AndroidTestCase",
+ "android/test/InstrumentationTestCase",
+ "android/test/InstrumentationTestSuite",
+ )
+}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt
index 25cad02..dc934eb 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt
@@ -183,7 +183,7 @@
av.visit("value", ravenwoodTestRunnerType.type)
av.visitEnd()
}
- log.d("Processed ${classInternalName.toHumanReadableClassName()}")
+ log.i("Update the @RunWith: ${classInternalName.toHumanReadableClassName()}")
}
/*