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()}")
     }
 
     /*