Cherry-pick Ravenwood "core" code
- Copied f/b/r and f/b/t/h
- Ported files under f/b/core, only what needed to for run-ravenwood-tests.sh to pass
- Local changes because of missing resoucres support
- Added @DisabledOnRavenwood(reason="AOSP is missing resources support") to tests under f/b/r that depends on resources
bivalentinst and servicestest
- Added try-catch around ResourcesManager.setInstance()
Flag: EXEMPT host test change only
Bug: 292141694
Test: $ANDROID_BUILD_TOP/frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh
Merged-in: I8a9b8374be3ae052ba4f152eb43af20d0871597f
Change-Id: Iefd574dbded8c4ab2e244c4918c26641364a3432
diff --git a/ravenwood/.gitignore b/ravenwood/.gitignore
new file mode 100644
index 0000000..751553b
--- /dev/null
+++ b/ravenwood/.gitignore
@@ -0,0 +1 @@
+*.bak
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index ac62687..10e4f38 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -5,6 +5,10 @@
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
+
+ // OWNER: g/ravenwood
+ // Bug component: 25698
+ default_team: "trendy_team_framework_backstage_power",
}
filegroup {
@@ -94,6 +98,9 @@
libs: [
"ravenwood-runtime-common-ravenwood",
],
+ static_libs: [
+ "framework-annotations-lib", // should it be "libs" instead?
+ ],
visibility: ["//visibility:private"],
}
@@ -126,8 +133,9 @@
],
libs: [
"framework-minus-apex.ravenwood",
- "ravenwood-junit",
+ "ravenwood-helper-libcore-runtime",
],
+ sdk_version: "core_current",
visibility: ["//visibility:private"],
}
@@ -160,11 +168,22 @@
"ravenwood-framework",
"services.core.ravenwood",
"junit",
+ "framework-annotations-lib",
+ "ravenwood-helper-framework-runtime",
+ "ravenwood-helper-libcore-runtime",
],
visibility: ["//frameworks/base"],
jarjar_rules: ":ravenwood-services-jarjar-rules",
}
+java_device_for_host {
+ name: "ravenwood-junit-impl-for-ravenizer",
+ libs: [
+ "ravenwood-junit-impl",
+ ],
+ visibility: [":__subpackages__"],
+}
+
// Separated out from ravenwood-junit-impl since it needs to compile
// against `module_current`
java_library {
@@ -202,6 +221,7 @@
libs: [
"junit",
"flag-junit",
+ "framework-annotations-lib",
],
visibility: ["//visibility:public"],
}
@@ -279,10 +299,10 @@
src: "scripts/ravenwood-stats-checker.sh",
test_suites: ["general-tests"],
data: [
- ":hoststubgen_framework-minus-apex_stats.csv",
- ":hoststubgen_framework-minus-apex_apis.csv",
- ":hoststubgen_framework-minus-apex_keep_all.txt",
- ":hoststubgen_framework-minus-apex_dump.txt",
+ ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_stats.csv}",
+ ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_apis.csv}",
+ ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_keep_all.txt}",
+ ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_dump.txt}",
":services.core.ravenwood-base{hoststubgen_services.core_stats.csv}",
":services.core.ravenwood-base{hoststubgen_services.core_apis.csv}",
":services.core.ravenwood-base{hoststubgen_services.core_keep_all.txt}",
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index 5754837..86246e2 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -1,20 +1,15 @@
-// Keep the following two TEST_MAPPINGs in sync:
-// frameworks/base/ravenwood/TEST_MAPPING
-// frameworks/base/tools/hoststubgen/TEST_MAPPING
{
"presubmit": [
{ "name": "tiny-framework-dump-test" },
{ "name": "hoststubgentest" },
+ { "name": "hoststubgen-test-tiny-test" },
{ "name": "hoststubgen-invoke-test" },
- {
- "name": "RavenwoodMockitoTest_device"
- },
- {
- "name": "RavenwoodBivalentTest_device"
- },
- {
- "name": "RavenwoodResApkTest"
- },
+ { "name": "RavenwoodMockitoTest_device" },
+ { "name": "RavenwoodBivalentTest_device" },
+
+ { "name": "RavenwoodBivalentInstTest_nonself_inst" },
+ { "name": "RavenwoodBivalentInstTest_self_inst_device" },
+
// The sysui tests should match vendor/unbundled_google/packages/SystemUIGoogle/TEST_MAPPING
{
"name": "SystemUIGoogleTests"
@@ -26,6 +21,128 @@
}
],
"ravenwood-presubmit": [
+ // AUTO-GENERATED-START
+ // DO NOT MODIFY MANUALLY
+ // Use scripts/update-test-mapping.sh to update it.
+ {
+ "name": "AdServicesSharedLibrariesUnitTestsRavenwood",
+ "host": true
+ },
+ {
+ "name": "android.test.mock.ravenwood.tests",
+ "host": true
+ },
+ {
+ "name": "CarLibHostUnitTest",
+ "host": true,
+ "keywords": ["automotive_code_coverage"]
+ },
+ {
+ "name": "CarServiceHostUnitTest",
+ "host": true,
+ "keywords": ["automotive_code_coverage"]
+ },
+ {
+ "name": "CarSystemUIRavenTests",
+ "host": true,
+ "keywords": ["automotive_code_coverage"]
+ },
+ {
+ "name": "CtsAccountManagerTestCasesRavenwood",
+ "host": true
+ },
+ {
+ "name": "CtsAppTestCasesRavenwood",
+ "host": true
+ },
+ {
+ "name": "CtsContentTestCasesRavenwood",
+ "host": true
+ },
+ {
+ "name": "CtsDatabaseTestCasesRavenwood",
+ "host": true
+ },
+ {
+ "name": "CtsGraphicsTestCasesRavenwood",
+ "host": true
+ },
+ {
+ "name": "CtsIcuTestCasesRavenwood",
+ "host": true
+ },
+ {
+ "name": "CtsInputMethodTestCasesRavenwood",
+ "host": true
+ },
+ {
+ "name": "CtsOsTestCasesRavenwood",
+ "host": true
+ },
+ {
+ "name": "CtsProtoTestCasesRavenwood",
+ "host": true
+ },
+ {
+ "name": "CtsResourcesTestCasesRavenwood",
+ "host": true
+ },
+ {
+ "name": "CtsTextTestCasesRavenwood",
+ "host": true
+ },
+ {
+ "name": "CtsUtilTestCasesRavenwood",
+ "host": true
+ },
+ {
+ "name": "FrameworksCoreSystemPropertiesTestsRavenwood",
+ "host": true
+ },
+ {
+ "name": "FrameworksCoreTestsRavenwood",
+ "host": true
+ },
+ {
+ "name": "FrameworksInputMethodSystemServerTestsRavenwood",
+ "host": true
+ },
+ {
+ "name": "FrameworksMockingServicesTestsRavenwood",
+ "host": true
+ },
+ {
+ "name": "FrameworksServicesTestsRavenwood",
+ "host": true
+ },
+ {
+ "name": "FrameworksUtilTestsRavenwood",
+ "host": true
+ },
+ {
+ "name": "InternalTestsRavenwood",
+ "host": true
+ },
+ {
+ "name": "PowerStatsTestsRavenwood",
+ "host": true
+ },
+ {
+ "name": "RavenwoodBivalentInstTest_nonself_inst",
+ "host": true
+ },
+ {
+ "name": "RavenwoodBivalentInstTest_self_inst",
+ "host": true
+ },
+ {
+ "name": "RavenwoodBivalentTest",
+ "host": true
+ },
+ {
+ "name": "RavenwoodCoreTest",
+ "host": true
+ },
{
"name": "RavenwoodMinimumTest",
"host": true
@@ -35,15 +152,22 @@
"host": true
},
{
- "name": "CtsUtilTestCasesRavenwood",
+ "name": "RavenwoodResApkTest",
"host": true
},
{
- "name": "RavenwoodCoreTest",
+ "name": "RavenwoodRuntimeTest",
"host": true
},
{
- "name": "RavenwoodBivalentTest",
+ "name": "RavenwoodServicesTest",
+ "host": true
+ }
+ // AUTO-GENERATED-END
+ ],
+ "ravenwood-postsubmit": [
+ {
+ "name": "SystemUiRavenTests",
"host": true
}
]
diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java
similarity index 82%
copy from ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
copy to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java
index 4b9cf85..b582ccf 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -15,7 +15,7 @@
*/
package android.ravenwood.annotation;
-import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.ElementType.METHOD;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -29,8 +29,7 @@
*
* @hide
*/
-@Target({TYPE})
+@Target({METHOD})
@Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodNativeSubstitutionClass {
- String value();
+public @interface RavenwoodRedirect {
}
diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java
similarity index 94%
rename from ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
rename to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java
index 4b9cf85..bee9222 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java
@@ -31,6 +31,6 @@
*/
@Target({TYPE})
@Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodNativeSubstitutionClass {
+public @interface RavenwoodRedirectionClass {
String value();
}
diff --git a/ravenwood/bivalenttest/Android.bp b/ravenwood/bivalenttest/Android.bp
index 06cf08e6..e897735 100644
--- a/ravenwood/bivalenttest/Android.bp
+++ b/ravenwood/bivalenttest/Android.bp
@@ -39,6 +39,9 @@
"androidx.test.ext.junit",
"androidx.test.rules",
+ "junit-params",
+ "platform-parametric-runner-lib",
+
// To make sure it won't cause VerifyError (b/324063814)
"platformprotosnano",
],
@@ -65,6 +68,9 @@
"androidx.test.ext.junit",
"androidx.test.rules",
+ "junit-params",
+ "platform-parametric-runner-lib",
+
"ravenwood-junit",
],
jni_libs: [
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java
index 3a24c0e..e8f59db 100644
--- a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java
@@ -26,6 +26,10 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+/**
+ * Test to ensure @DisabledOnRavenwood works. Note, now the DisabledOnRavenwood annotation
+ * is handled by the test runner, so it won't really need the class rule.
+ */
@RunWith(AndroidJUnit4.class)
@DisabledOnRavenwood
public class RavenwoodClassRuleDeviceOnlyTest {
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleRavenwoodOnlyTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleRavenwoodOnlyTest.java
deleted file mode 100644
index aa33dc3..0000000
--- a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleRavenwoodOnlyTest.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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.ravenwoodtest.bivalenttest;
-
-import android.platform.test.ravenwood.RavenwoodClassRule;
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Assert;
-import org.junit.ClassRule;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-// TODO: atest RavenwoodBivalentTest_device fails with the following message.
-// `RUNNER ERROR: Instrumentation reported numtests=7 but only ran 6`
-// @android.platform.test.annotations.DisabledOnNonRavenwood
-// Figure it out and then make DisabledOnNonRavenwood support TYPEs as well.
-@Ignore
-public class RavenwoodClassRuleRavenwoodOnlyTest {
- @ClassRule
- public static final RavenwoodClassRule sRavenwood = new RavenwoodClassRule();
-
- @Test
- public void testRavenwoodOnly() {
- Assert.assertTrue(RavenwoodRule.isOnRavenwood());
- }
-}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java
new file mode 100644
index 0000000..a5a16c1
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.ravenwoodtest.bivalenttest;
+
+import static android.platform.test.ravenwood.RavenwoodConfig.isOnRavenwood;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.ravenwood.RavenwoodConfig;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test to make sure the config field is used.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodConfigTest {
+ private static final String PACKAGE_NAME = "com.test";
+
+ @RavenwoodConfig.Config
+ public static RavenwoodConfig sConfig =
+ new RavenwoodConfig.Builder()
+ .setPackageName(PACKAGE_NAME)
+ .build();
+
+ @Test
+ public void testConfig() {
+ assumeTrue(isOnRavenwood());
+ assertEquals(PACKAGE_NAME,
+ InstrumentationRegistry.getInstrumentation().getContext().getPackageName());
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java
new file mode 100644
index 0000000..c25d2b4
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.ravenwoodtest.bivalenttest;
+
+import android.platform.test.ravenwood.RavenwoodConfig;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Assume;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+/**
+ * Make sure having multiple RavenwoodRule's is detected.
+ * (But only when running on ravenwod. Otherwise it'll be ignored.)
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodMultipleRuleTest {
+
+ @Rule(order = Integer.MIN_VALUE)
+ public final ExpectedException mExpectedException = ExpectedException.none();
+
+ @Rule
+ public final RavenwoodRule mRavenwood1 = new RavenwoodRule();
+
+ @Rule
+ public final RavenwoodRule mRavenwood2 = new RavenwoodRule();
+
+ public RavenwoodMultipleRuleTest() {
+ // We can't call it within the test method because the exception happens before
+ // calling the method, so set it up here.
+ if (RavenwoodConfig.isOnRavenwood()) {
+ mExpectedException.expectMessage("Multiple nesting RavenwoodRule");
+ }
+ }
+
+ @Test
+ public void testMultipleRulesNotAllowed() {
+ Assume.assumeTrue(RavenwoodConfig.isOnRavenwood());
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNoConfigNoRuleTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNoConfigNoRuleTest.java
new file mode 100644
index 0000000..d473305
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNoConfigNoRuleTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.ravenwoodtest.bivalenttest;
+
+import static org.junit.Assert.assertNotNull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test to make sure the environment is still initialized when no config and no rules are set.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodNoConfigNoRuleTest {
+
+ @Test
+ public void testInitialization() {
+ assertNotNull(InstrumentationRegistry.getInstrumentation());
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java
index 01e90d8..3de372e 100644
--- a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java
@@ -15,7 +15,6 @@
*/
package com.android.ravenwoodtest.bivalenttest;
-import android.platform.test.annotations.DisabledOnNonRavenwood;
import android.platform.test.annotations.DisabledOnRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
import android.util.Log;
@@ -39,12 +38,6 @@
}
@Test
- @DisabledOnNonRavenwood
- public void testRavenwoodOnly() {
- Assert.assertTrue(RavenwoodRule.isOnRavenwood());
- }
-
- @Test
public void testDumpSystemProperties() {
Log.w("XXX", "System properties");
for (var sp : System.getProperties().entrySet()) {
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java
new file mode 100644
index 0000000..09a0aa8
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java
@@ -0,0 +1,110 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.fail;
+
+import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
+
+import android.util.Log;
+
+import java.lang.StackWalker.StackFrame;
+import java.util.HashMap;
+
+/**
+ * Used to keep track of and count the number of calls.
+ */
+public class CallTracker {
+ public static final String TAG = "CallTracker";
+
+ private final HashMap<String, Integer> mNumCalled = new HashMap<>();
+
+ /**
+ * Call it when a method is called. It increments the count for the calling method.
+ */
+ public void incrementMethodCallCount() {
+ var methodName = getCallingMethodName(1);
+
+ Log.i(TAG, "Method called: " + methodName);
+
+ mNumCalled.put(methodName, getNumCalled(methodName) + 1);
+ }
+
+ /**
+ * Return the number of calls of a method.
+ */
+ public int getNumCalled(String methodName) {
+ return mNumCalled.getOrDefault(methodName, 0);
+ }
+
+ /**
+ * Return the current method name. (with the class name.)
+ */
+ private static String getCallingMethodName(int frameOffset) {
+ var walker = StackWalker.getInstance(RETAIN_CLASS_REFERENCE);
+ var caller = walker.walk(frames ->
+ frames.skip(1 + frameOffset).findFirst().map(StackFrame::getMethodName)
+ );
+ return caller.get();
+ }
+
+ /**
+ * Check the number of calls stored in {@link #mNumCalled}.
+ */
+ public void assertCalls(Object... methodNameAndCountPairs) {
+ // Create a local copy
+ HashMap<String, Integer> counts = new HashMap<>(mNumCalled);
+ for (int i = 0; i < methodNameAndCountPairs.length - 1; i += 2) {
+ String methodName = (String) methodNameAndCountPairs[i];
+ int expectedCount = (Integer) methodNameAndCountPairs[i + 1];
+
+ if (getNumCalled(methodName) != expectedCount) {
+ fail(String.format("Method %s: expected call count=%d, actual=%d",
+ methodName, expectedCount, getNumCalled(methodName)));
+ }
+ counts.remove(methodName);
+ }
+ // All other entries are expected to be 0.
+ var sb = new StringBuilder();
+ for (var e : counts.entrySet()) {
+ if (e.getValue() == 0) {
+ continue;
+ }
+ sb.append(String.format("Method %s: expected call count=0, actual=%d",
+ e.getKey(), e.getValue()));
+ }
+ if (sb.length() > 0) {
+ fail(sb.toString());
+ }
+ }
+
+ /**
+ * Same as {@link #assertCalls(Object...)} but it kills the process if it fails.
+ * Only use in @AfterClass.
+ */
+ public void assertCallsOrDie(Object... methodNameAndCountPairs) {
+ try {
+ assertCalls(methodNameAndCountPairs);
+ } catch (Throwable th) {
+ // TODO: I don't think it's by spec, but the exception here would be ignored both on
+ // ravenwood and on the device side. Look into it.
+ Log.e(TAG, "*** Failure detected in @AfterClass! ***", th);
+ Log.e(TAG, "JUnit seems to ignore exceptions from @AfterClass, so killing self.");
+ System.exit(7);
+ }
+ }
+
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java
new file mode 100644
index 0000000..d7c2c6c
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.assertFalse;
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.RavenwoodTestRunnerInitializing;
+import android.platform.test.ravenwood.RavenwoodRule;
+import android.util.Log;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Make sure RavenwoodAwareTestRunnerTest properly delegates to the original runner,
+ * and also run the special annotated methods.
+ */
+@RunWith(JUnitParamsRunner.class)
+public class RavenwoodAwareTestRunnerTest {
+ public static final String TAG = "RavenwoodAwareTestRunnerTest";
+
+ private static final CallTracker sCallTracker = new CallTracker();
+
+ private static int getExpectedRavenwoodRunnerInitializingNumCalls() {
+ return RavenwoodRule.isOnRavenwood() ? 1 : 0;
+ }
+
+ @RavenwoodTestRunnerInitializing
+ public static void ravenwoodRunnerInitializing() {
+ // No other calls should have been made.
+ sCallTracker.assertCalls();
+
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @BeforeClass
+ public static void beforeClass() {
+ sCallTracker.assertCalls(
+ "ravenwoodRunnerInitializing",
+ getExpectedRavenwoodRunnerInitializingNumCalls()
+ );
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @Test
+ public void test1() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @Test
+ @Parameters({"foo", "bar"})
+ public void testWithParams(String arg) {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @Test
+ @DisabledOnRavenwood
+ public void testDeviceOnly() {
+ assertFalse(RavenwoodRule.isOnRavenwood());
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ Log.i(TAG, "afterClass called");
+
+ sCallTracker.assertCallsOrDie(
+ "ravenwoodRunnerInitializing",
+ getExpectedRavenwoodRunnerInitializingNumCalls(),
+ "beforeClass", 1,
+ "test1", 1,
+ "testWithParams", 2
+ );
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java
new file mode 100644
index 0000000..7ef672e
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@DisabledOnRavenwood
+public class RavenwoodImplicitClassRuleDeviceOnlyTest {
+ public static final String TAG = "RavenwoodImplicitClassRuleDeviceOnlyTest";
+
+ @BeforeClass
+ public static void beforeClass() {
+ // This method shouldn't be called -- unless RUN_DISABLED_TESTS is enabled.
+
+ // If we're doing RUN_DISABLED_TESTS, don't throw here, because that'd confuse junit.
+ if (!RavenwoodRule.private$ravenwood().isRunningDisabledTests()) {
+ Assert.assertFalse(RavenwoodRule.isOnRavenwood());
+ }
+ }
+
+ @Test
+ public void testDeviceOnly() {
+ Assert.assertFalse(RavenwoodRule.isOnRavenwood());
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ if (RavenwoodRule.isOnRavenwood()) {
+ Log.e(TAG, "Even @AfterClass shouldn't be executed!");
+
+ if (!RavenwoodRule.private$ravenwood().isRunningDisabledTests()) {
+ System.exit(1);
+ }
+ }
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java
new file mode 100644
index 0000000..7ef40dc
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Assume;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+
+/**
+ * Make sure ravenizer will inject implicit rules and rewrite the existing rules' orders.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodImplicitRuleOrderRewriteTest {
+
+ private static final TestRule sEmptyRule = (statement, description) -> statement;
+
+ // We have two sets of 9 rules below, for class rules and instance rules.
+ // - Ravenizer will inject 2 more rules of each kind.
+ // - Ravenizer will adjust their order, so even though we'll add two sets of class and instance
+ // rules with a MIN / MAX order, there will still be no duplicate in the order.
+
+ private static final int EXPECTED_RULE_COUNT = 9 + 2;
+
+ @ClassRule(order = Integer.MIN_VALUE)
+ public static final TestRule sRule01 = sEmptyRule;
+
+ @ClassRule(order = Integer.MIN_VALUE + 1)
+ public static final TestRule sRule02 = sEmptyRule;
+
+ @ClassRule(order = -10)
+ public static final TestRule sRule03 = sEmptyRule;
+
+ @ClassRule(order = -1)
+ public static final TestRule sRule04 = sEmptyRule;
+
+ @ClassRule(order = 0)
+ public static final TestRule sRule05 = sEmptyRule;
+
+ @ClassRule(order = 1)
+ public static final TestRule sRule06 = sEmptyRule;
+
+ @ClassRule(order = 10)
+ public static final TestRule sRule07 = sEmptyRule;
+
+ @ClassRule(order = Integer.MAX_VALUE - 1)
+ public static final TestRule sRule08 = sEmptyRule;
+
+ @ClassRule(order = Integer.MAX_VALUE)
+ public static final TestRule sRule09 = sEmptyRule;
+
+ @Rule(order = Integer.MIN_VALUE)
+ public final TestRule mRule01 = sEmptyRule;
+
+ @Rule(order = Integer.MIN_VALUE + 1)
+ public final TestRule mRule02 = sEmptyRule;
+
+ @Rule(order = -10)
+ public final TestRule mRule03 = sEmptyRule;
+
+ @Rule(order = -1)
+ public final TestRule mRule04 = sEmptyRule;
+
+ @Rule(order = 0)
+ public final TestRule mRule05 = sEmptyRule;
+
+ @Rule(order = 1)
+ public final TestRule mRule06 = sEmptyRule;
+
+ @Rule(order = 10)
+ public final TestRule mRule07 = sEmptyRule;
+
+ @Rule(order = Integer.MAX_VALUE - 1)
+ public final TestRule mRule08 = sEmptyRule;
+
+ @Rule(order = Integer.MAX_VALUE)
+ public final TestRule mRule09 = sEmptyRule;
+
+ private void checkRules(boolean classRule) {
+ final var anotClass = classRule ? ClassRule.class : Rule.class;
+
+ final HashMap<Integer, Integer> ordersUsed = new HashMap<>();
+
+ for (var field : this.getClass().getDeclaredFields()) {
+ if (!field.isAnnotationPresent(anotClass)) {
+ continue;
+ }
+ final var anot = field.getAnnotation(anotClass);
+ final int order = classRule ? ((ClassRule) anot).order() : ((Rule) anot).order();
+
+ if (ordersUsed.containsKey(order)) {
+ fail("Detected duplicate order=" + order);
+ }
+ ordersUsed.put(order, 1);
+ }
+ assertEquals(EXPECTED_RULE_COUNT, ordersUsed.size());
+ }
+
+ @Test
+ public void testClassRules() {
+ Assume.assumeTrue(RavenwoodRule.isOnRavenwood());
+
+ checkRules(true);
+ }
+
+ @Test
+ public void testInstanceRules() {
+ Assume.assumeTrue(RavenwoodRule.isOnRavenwood());
+
+ checkRules(false);
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java
new file mode 100644
index 0000000..ae596b1
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test to make sure when a test class inherits another test class, the base class's
+ * implicit rules are shadowed and won't be executed.
+ *
+ * ... But for now, we don't have a way to programmatically check it, so for now we need to
+ * check the log file manually.
+ *
+ * TODO: Implement the test.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodImplicitRuleShadowingTest extends RavenwoodImplicitRuleShadowingTestBase {
+ @Test
+ public void testOkInSubClass() {
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java
new file mode 100644
index 0000000..1ca97af
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java
@@ -0,0 +1,31 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * A test class that's just inherited by RavenwoodImplicitRuleShadowingTest.
+ */
+@RunWith(AndroidJUnit4.class)
+public abstract class RavenwoodImplicitRuleShadowingTestBase {
+ @Test
+ public void testOkInBaseClass() {
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodNoRavenizerTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodNoRavenizerTest.java
new file mode 100644
index 0000000..9d878f4
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodNoRavenizerTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import android.platform.test.annotations.NoRavenizer;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.RavenwoodTestRunnerInitializing;
+
+import org.junit.Test;
+
+/**
+ * Test for {@link android.platform.test.annotations.NoRavenizer}
+ */
+@NoRavenizer
+public class RavenwoodNoRavenizerTest {
+ public static final String TAG = "RavenwoodNoRavenizerTest";
+
+ private static final CallTracker sCallTracker = new CallTracker();
+
+ /**
+ * With @NoRavenizer, this method shouldn't be called.
+ */
+ @RavenwoodTestRunnerInitializing
+ public static void ravenwoodRunnerInitializing() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ /**
+ * Make sure ravenwoodRunnerInitializing() wasn't called.
+ */
+ @Test
+ public void testNotRavenized() {
+ sCallTracker.assertCalls(
+ "ravenwoodRunnerInitializing", 0
+ );
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java
new file mode 100644
index 0000000..c77841b
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.fail;
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.RavenwoodTestRunnerInitializing;
+import android.platform.test.ravenwood.RavenwoodRule;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.AfterClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for "RAVENWOOD_RUN_DISABLED_TESTS" with "REALLY_DISABLED" set.
+ *
+ * This test is only executed on Ravenwood.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodRunDisabledTestsReallyDisabledTest {
+ private static final String TAG = "RavenwoodRunDisabledTestsTest";
+
+ private static final CallTracker sCallTracker = new CallTracker();
+
+ @RavenwoodTestRunnerInitializing
+ public static void ravenwoodRunnerInitializing() {
+ RavenwoodRule.private$ravenwood().overrideRunDisabledTest(true,
+ "\\#testReallyDisabled$");
+ }
+
+ /**
+ * This test gets to run with RAVENWOOD_RUN_DISABLED_TESTS set.
+ */
+ @Test
+ @DisabledOnRavenwood
+ public void testDisabledTestGetsToRun() {
+ if (!RavenwoodRule.isOnRavenwood()) {
+ return;
+ }
+ sCallTracker.incrementMethodCallCount();
+
+ fail("This test won't pass on Ravenwood.");
+ }
+
+ /**
+ * This will still not be executed due to the "really disabled" pattern.
+ */
+ @Test
+ @DisabledOnRavenwood
+ public void testReallyDisabled() {
+ if (!RavenwoodRule.isOnRavenwood()) {
+ return;
+ }
+ sCallTracker.incrementMethodCallCount();
+
+ fail("This test won't pass on Ravenwood.");
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ if (!RavenwoodRule.isOnRavenwood()) {
+ return;
+ }
+ Log.i(TAG, "afterClass called");
+
+ sCallTracker.assertCallsOrDie(
+ "testDisabledTestGetsToRun", 1,
+ "testReallyDisabled", 0
+ );
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java
new file mode 100644
index 0000000..ea1a29d
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.fail;
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.RavenwoodTestRunnerInitializing;
+import android.platform.test.ravenwood.RavenwoodRule;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.AfterClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for "RAVENWOOD_RUN_DISABLED_TESTS". (with no "REALLY_DISABLED" set.)
+ *
+ * This test is only executed on Ravenwood.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodRunDisabledTestsTest {
+ private static final String TAG = "RavenwoodRunDisabledTestsTest";
+
+ @Rule
+ public ExpectedException mExpectedException = ExpectedException.none();
+
+ private static final CallTracker sCallTracker = new CallTracker();
+
+ @RavenwoodTestRunnerInitializing
+ public static void ravenwoodRunnerInitializing() {
+ RavenwoodRule.private$ravenwood().overrideRunDisabledTest(true, null);
+ }
+
+ @Test
+ @DisabledOnRavenwood
+ public void testDisabledTestGetsToRun() {
+ if (!RavenwoodRule.isOnRavenwood()) {
+ return;
+ }
+ sCallTracker.incrementMethodCallCount();
+
+ fail("This test won't pass on Ravenwood.");
+ }
+
+ @Test
+ @DisabledOnRavenwood
+ public void testDisabledButPass() {
+ if (!RavenwoodRule.isOnRavenwood()) {
+ return;
+ }
+ sCallTracker.incrementMethodCallCount();
+
+ // When a @DisabledOnRavenwood actually passed, the runner should make fail().
+ mExpectedException.expectMessage("it actually passed under Ravenwood");
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ if (!RavenwoodRule.isOnRavenwood()) {
+ return;
+ }
+ Log.i(TAG, "afterClass called");
+
+ sCallTracker.assertCallsOrDie(
+ "testDisabledTestGetsToRun", 1,
+ "testDisabledButPass", 1
+ );
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java
new file mode 100644
index 0000000..c042eb0
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Make sure ravenwood's test runner works with {@link AndroidJUnit4}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodRunnerWithAndroidXRunnerTest {
+ public static final String TAG = "RavenwoodRunnerWithAndroidXRunnerTest";
+
+ private static final CallTracker sCallTracker = new CallTracker();
+
+ @BeforeClass
+ public static void beforeClass() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @Before
+ public void beforeTest() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @After
+ public void afterTest() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @Test
+ public void test1() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @Test
+ public void test2() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ Log.i(TAG, "afterClass called");
+
+ sCallTracker.assertCallsOrDie(
+ "beforeClass", 1,
+ "beforeTest", 2,
+ "afterTest", 2,
+ "test1", 1,
+ "test2", 1
+ );
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java
new file mode 100644
index 0000000..2feb5ba
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Make sure ravenwood's test runner works with {@link AndroidJUnit4}.
+ */
+@RunWith(JUnitParamsRunner.class)
+public class RavenwoodRunnerWithJUnitParamsRunnerTest {
+ public static final String TAG = "RavenwoodRunnerTest";
+
+ private static final CallTracker sCallTracker = new CallTracker();
+
+ @BeforeClass
+ public static void beforeClass() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @Before
+ public void beforeTest() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @After
+ public void afterTest() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @Test
+ public void testWithNoParams() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @Test
+ @Parameters({"foo", "bar"})
+ public void testWithParams(String arg) {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ Log.i(TAG, "afterClass called");
+
+ sCallTracker.assertCallsOrDie(
+ "beforeClass", 1,
+ "beforeTest", 3,
+ "afterTest", 3,
+ "testWithNoParams", 1,
+ "testWithParams", 2
+ );
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java
new file mode 100644
index 0000000..7e3bc0f
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java
@@ -0,0 +1,93 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Make sure ravenwood's test runner works with {@link ParameterizedAndroidJunit4}.
+ */
+@RunWith(ParameterizedAndroidJunit4.class)
+public class RavenwoodRunnerWithParameterizedAndroidJunit4Test {
+ public static final String TAG = "RavenwoodRunnerTest";
+
+ private static final CallTracker sCallTracker = new CallTracker();
+
+ private final String mParam;
+
+ private static int sNumInsantiation = 0;
+
+ public RavenwoodRunnerWithParameterizedAndroidJunit4Test(String param) {
+ mParam = param;
+ sNumInsantiation++;
+ }
+
+ @BeforeClass
+ public static void beforeClass() {
+ // It seems like ParameterizedAndroidJunit4 calls the @BeforeTest / @AfterTest methods
+ // one time too many.
+ // With two parameters, this method should be called only twice, but it's actually
+ // called three times.
+ // So let's not check the number fo beforeClass calls.
+ }
+
+ @Before
+ public void beforeTest() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @After
+ public void afterTest() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @Parameters
+ public static List<String> getParams() {
+ var params = new ArrayList<String>();
+ params.add("foo");
+ params.add("bar");
+ return params;
+ }
+
+ @Test
+ public void testWithParams() {
+ sCallTracker.incrementMethodCallCount();
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ Log.i(TAG, "afterClass called");
+
+ sCallTracker.assertCallsOrDie(
+ "beforeTest", sNumInsantiation,
+ "afterTest", sNumInsantiation,
+ "testWithParams", sNumInsantiation
+ );
+ }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodSuiteTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodSuiteTest.java
new file mode 100644
index 0000000..7e396c2
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodSuiteTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import android.util.Log;
+
+import org.junit.AfterClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * Test to make sure {@link Suite} works with the ravenwood test runner.
+ */
+@RunWith(Suite.class)
[email protected]({
+ RavenwoodSuiteTest.Test1.class,
+ RavenwoodSuiteTest.Test2.class
+})
+public class RavenwoodSuiteTest {
+ public static final String TAG = "RavenwoodSuiteTest";
+
+ private static final CallTracker sCallTracker = new CallTracker();
+
+ @AfterClass
+ public static void afterClass() {
+ Log.i(TAG, "afterClass called");
+
+ sCallTracker.assertCallsOrDie(
+ "test1", 1,
+ "test2", 1
+ );
+ }
+
+ /**
+ * Workaround for the issue where tradefed won't think a class is a test class
+ * if it has a @RunWith but no @Test methods, even if it is a Suite.
+ */
+ @Test
+ public void testEmpty() {
+ }
+
+ public static class Test1 {
+ @Test
+ public void test1() {
+ sCallTracker.incrementMethodCallCount();
+ }
+ }
+
+ public static class Test2 {
+ @Test
+ public void test2() {
+ sCallTracker.incrementMethodCallCount();
+ }
+ }
+}
diff --git a/ravenwood/coretest/README.md b/ravenwood/coretest/README.md
deleted file mode 100644
index b60bfbf..0000000
--- a/ravenwood/coretest/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Ravenwood core test
-
-This test contains (non-bivalent) tests for Ravenwood itself -- e.g. tests for the ravenwood rules.
\ No newline at end of file
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java
deleted file mode 100644
index f1e33cb..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.ravenwoodtest.coretest;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4; // Intentionally use the deprecated one.
-
-import org.junit.Assume;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * Test for the test runner validator in RavenwoodRule.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestRunnerValidationTest {
- // Note the following rules don't have a @Rule, because they need to be applied in a specific
- // order. So we use a RuleChain instead.
- private ExpectedException mThrown = ExpectedException.none();
- private final RavenwoodRule mRavenwood = new RavenwoodRule();
-
- @Rule
- public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
-
- public RavenwoodTestRunnerValidationTest() {
- Assume.assumeTrue(RavenwoodRule._$RavenwoodPrivate.isOptionalValidationEnabled());
- // Because RavenwoodRule will throw this error before executing the test method,
- // we can't do it in the test method itself.
- // So instead, we initialize it here.
- mThrown.expectMessage("Switch to androidx.test.ext.junit.runners.AndroidJUnit4");
- }
-
- @Test
- public void testValidateTestRunner() {
- }
-}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java
deleted file mode 100644
index db95fad..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.ravenwoodtest.coretest.methodvalidation;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
- * This class contains tests for this validator.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestMethodValidation_Fail01_Test {
- private ExpectedException mThrown = ExpectedException.none();
- private final RavenwoodRule mRavenwood = new RavenwoodRule();
-
- @Rule
- public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
-
- public RavenwoodTestMethodValidation_Fail01_Test() {
- mThrown.expectMessage("Method setUp() doesn't have @Before");
- }
-
- @SuppressWarnings("JUnit4SetUpNotRun")
- public void setUp() {
- }
-
- @Test
- public void testEmpty() {
- }
-}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java
deleted file mode 100644
index ddc66c7..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.ravenwoodtest.coretest.methodvalidation;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
- * This class contains tests for this validator.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestMethodValidation_Fail02_Test {
- private ExpectedException mThrown = ExpectedException.none();
- private final RavenwoodRule mRavenwood = new RavenwoodRule();
-
- @Rule
- public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
-
- public RavenwoodTestMethodValidation_Fail02_Test() {
- mThrown.expectMessage("Method tearDown() doesn't have @After");
- }
-
- @SuppressWarnings("JUnit4TearDownNotRun")
- public void tearDown() {
- }
-
- @Test
- public void testEmpty() {
- }
-}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java
deleted file mode 100644
index ec8e907..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.ravenwoodtest.coretest.methodvalidation;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
- * This class contains tests for this validator.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestMethodValidation_Fail03_Test {
- private ExpectedException mThrown = ExpectedException.none();
- private final RavenwoodRule mRavenwood = new RavenwoodRule();
-
- @Rule
- public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
-
- public RavenwoodTestMethodValidation_Fail03_Test() {
- mThrown.expectMessage("Method testFoo() doesn't have @Test");
- }
-
- @SuppressWarnings("JUnit4TestNotRun")
- public void testFoo() {
- }
-
- @Test
- public void testEmpty() {
- }
-}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java
deleted file mode 100644
index d952d07..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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.ravenwoodtest.coretest.methodvalidation;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
- * This class contains tests for this validator.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestMethodValidation_OkTest {
- @Rule
- public final RavenwoodRule mRavenwood = new RavenwoodRule();
-
- @Before
- public void setUp() {
- }
-
- @Before
- public void testSetUp() {
- }
-
- @After
- public void tearDown() {
- }
-
- @After
- public void testTearDown() {
- }
-
- @Test
- public void testEmpty() {
- }
-}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
new file mode 100644
index 0000000..478bead
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
@@ -0,0 +1,199 @@
+/*
+ * 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 android.platform.test.ravenwood;
+
+import static org.junit.Assert.fail;
+
+import android.os.Bundle;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Order;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Scope;
+import android.platform.test.ravenwood.RavenwoodTestStats.Result;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.runner.Description;
+import org.junit.runners.model.TestClass;
+
+/**
+ * Provide hook points created by {@link RavenwoodAwareTestRunner}.
+ *
+ * States are associated with each {@link RavenwoodAwareTestRunner} are stored in
+ * {@link RavenwoodRunnerState}, rather than as members of {@link RavenwoodAwareTestRunner}.
+ * See its javadoc for the reasons.
+ *
+ * All methods in this class must be called from the test main thread.
+ */
+public class RavenwoodAwareTestRunnerHook {
+ private static final String TAG = RavenwoodAwareTestRunner.TAG;
+
+ private RavenwoodAwareTestRunnerHook() {
+ }
+
+ /**
+ * Called before any code starts. Internally it will only initialize the environment once.
+ */
+ public static void performGlobalInitialization() {
+ RavenwoodRuntimeEnvironmentController.globalInitOnce();
+ }
+
+ /**
+ * Called when a runner starts, before the inner runner gets a chance to run.
+ */
+ public static void onRunnerInitializing(RavenwoodAwareTestRunner runner, TestClass testClass) {
+ Log.i(TAG, "onRunnerInitializing: testClass=" + testClass.getJavaClass()
+ + " runner=" + runner);
+
+ // This is needed to make AndroidJUnit4ClassRunner happy.
+ InstrumentationRegistry.registerInstance(null, Bundle.EMPTY);
+ }
+
+ /**
+ * Called when a whole test class is skipped.
+ */
+ public static void onClassSkipped(Description description) {
+ Log.i(TAG, "onClassSkipped: description=" + description);
+ RavenwoodTestStats.getInstance().onClassSkipped(description);
+ }
+
+ /**
+ * Called before the inner runner starts.
+ */
+ public static void onBeforeInnerRunnerStart(
+ RavenwoodAwareTestRunner runner, Description description) throws Throwable {
+ Log.v(TAG, "onBeforeInnerRunnerStart: description=" + description);
+
+ // Prepare the environment before the inner runner starts.
+ runner.mState.enterTestClass(description);
+ }
+
+ /**
+ * Called after the inner runner finished.
+ */
+ public static void onAfterInnerRunnerFinished(
+ RavenwoodAwareTestRunner runner, Description description) throws Throwable {
+ Log.v(TAG, "onAfterInnerRunnerFinished: description=" + description);
+
+ RavenwoodTestStats.getInstance().onClassFinished(description);
+ runner.mState.exitTestClass();
+ }
+
+ /**
+ * Called before a test / class.
+ *
+ * Return false if it should be skipped.
+ */
+ public static boolean onBefore(RavenwoodAwareTestRunner runner, Description description,
+ Scope scope, Order order) throws Throwable {
+ Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order);
+
+ if (scope == Scope.Instance && order == Order.Outer) {
+ // Start of a test method.
+ runner.mState.enterTestMethod(description);
+ }
+
+ final var classDescription = runner.mState.getClassDescription();
+
+ // Class-level annotations are checked by the runner already, so we only check
+ // method-level annotations here.
+ if (scope == Scope.Instance && order == Order.Outer) {
+ if (!RavenwoodEnablementChecker.shouldEnableOnRavenwood(
+ description, true)) {
+ RavenwoodTestStats.getInstance().onTestFinished(
+ classDescription, description, Result.Skipped);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Called after a test / class.
+ *
+ * Return false if the exception should be ignored.
+ */
+ public static boolean onAfter(RavenwoodAwareTestRunner runner, Description description,
+ Scope scope, Order order, Throwable th) {
+ Log.v(TAG, "onAfter: description=" + description + ", " + scope + ", " + order + ", " + th);
+
+ final var classDescription = runner.mState.getClassDescription();
+
+ if (scope == Scope.Instance && order == Order.Outer) {
+ // End of a test method.
+ runner.mState.exitTestMethod();
+ RavenwoodTestStats.getInstance().onTestFinished(classDescription, description,
+ th == null ? Result.Passed : Result.Failed);
+ }
+
+ // If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error.
+ if (RavenwoodRule.private$ravenwood().isRunningDisabledTests()
+ && scope == Scope.Instance && order == Order.Outer) {
+
+ boolean isTestEnabled = RavenwoodEnablementChecker.shouldEnableOnRavenwood(
+ description, false);
+ if (th == null) {
+ // Test passed. Is the test method supposed to be enabled?
+ if (isTestEnabled) {
+ // Enabled and didn't throw, okay.
+ return true;
+ } else {
+ // Disabled and didn't throw. We should report it.
+ fail("Test wasn't included under Ravenwood, but it actually "
+ + "passed under Ravenwood; consider updating annotations");
+ return true; // unreachable.
+ }
+ } else {
+ // Test failed.
+ if (isTestEnabled) {
+ // Enabled but failed. We should throw the exception.
+ return true;
+ } else {
+ // Disabled and failed. Expected. Don't throw.
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Called by {@link RavenwoodAwareTestRunner} to see if it should run a test class or not.
+ */
+ public static boolean shouldRunClassOnRavenwood(Class<?> clazz) {
+ return RavenwoodEnablementChecker.shouldRunClassOnRavenwood(clazz, true);
+ }
+
+ /**
+ * Called by RavenwoodRule.
+ */
+ public static void onRavenwoodRuleEnter(RavenwoodAwareTestRunner runner,
+ Description description, RavenwoodRule rule) throws Throwable {
+ Log.v(TAG, "onRavenwoodRuleEnter: description=" + description);
+
+ runner.mState.enterRavenwoodRule(rule);
+ }
+
+
+ /**
+ * Called by RavenwoodRule.
+ */
+ public static void onRavenwoodRuleExit(RavenwoodAwareTestRunner runner,
+ Description description, RavenwoodRule rule) throws Throwable {
+ Log.v(TAG, "onRavenwoodRuleExit: description=" + description);
+
+ runner.mState.exitRavenwoodRule(rule);
+ }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodConfigState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodConfigState.java
new file mode 100644
index 0000000..3535cb2
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodConfigState.java
@@ -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 android.platform.test.ravenwood;
+
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_EMPTY_RESOURCES_APK;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.Nullable;
+import android.app.ResourcesManager;
+import android.content.res.Resources;
+import android.view.DisplayAdjustments;
+
+import java.io.File;
+import java.util.HashMap;
+
+/**
+ * Used to store various states associated with {@link RavenwoodConfig} that's inly needed
+ * in junit-impl.
+ *
+ * We don't want to put it in junit-src to avoid having to recompile all the downstream
+ * dependencies after changing this class.
+ *
+ * All members must be called from the runner's main thread.
+ */
+public class RavenwoodConfigState {
+ private static final String TAG = "RavenwoodConfigState";
+
+ private final RavenwoodConfig mConfig;
+
+ public RavenwoodConfigState(RavenwoodConfig config) {
+ mConfig = config;
+ }
+
+ /** Map from path -> resources. */
+ private final HashMap<File, Resources> mCachedResources = new HashMap<>();
+
+ /**
+ * Load {@link Resources} from an APK, with cache.
+ */
+ public Resources loadResources(@Nullable File apkPath) {
+ var cached = mCachedResources.get(apkPath);
+ if (cached != null) {
+ return cached;
+ }
+
+ var fileToLoad = apkPath != null ? apkPath : new File(RAVENWOOD_EMPTY_RESOURCES_APK);
+
+ assertTrue("File " + fileToLoad + " doesn't exist.", fileToLoad.isFile());
+
+ final String path = fileToLoad.getAbsolutePath();
+ final var emptyPaths = new String[0];
+
+ ResourcesManager.getInstance().initializeApplicationPaths(path, emptyPaths);
+
+ final var ret = ResourcesManager.getInstance().getResources(null, path,
+ emptyPaths, emptyPaths, emptyPaths,
+ emptyPaths, null, null,
+ new DisplayAdjustments().getCompatibilityInfo(),
+ RavenwoodRuntimeEnvironmentController.class.getClassLoader(), null);
+
+ assertNotNull(ret);
+
+ mCachedResources.put(apkPath, ret);
+ return ret;
+ }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
index 1dd5e1d..239c806 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
@@ -16,8 +16,13 @@
package android.platform.test.ravenwood;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RESOURCE_APK;
+
import android.content.ClipboardManager;
import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
import android.hardware.ISerialManager;
import android.hardware.SerialManager;
import android.os.Handler;
@@ -31,11 +36,18 @@
import android.util.ArrayMap;
import android.util.Singleton;
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.File;
+import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
public class RavenwoodContext extends RavenwoodBaseContext {
+ private static final String TAG = "Ravenwood";
+
+ private final Object mLock = new Object();
private final String mPackageName;
private final HandlerThread mMainThread;
@@ -44,15 +56,31 @@
private final ArrayMap<Class<?>, String> mClassToName = new ArrayMap<>();
private final ArrayMap<String, Supplier<?>> mNameToFactory = new ArrayMap<>();
+ private final File mFilesDir;
+ private final File mCacheDir;
+ private final Supplier<Resources> mResourcesSupplier;
+
+ private RavenwoodContext mAppContext;
+
+ @GuardedBy("mLock")
+ private Resources mResources;
+
+ @GuardedBy("mLock")
+ private Resources.Theme mTheme;
+
private void registerService(Class<?> serviceClass, String serviceName,
Supplier<?> serviceSupplier) {
mClassToName.put(serviceClass, serviceName);
mNameToFactory.put(serviceName, serviceSupplier);
}
- public RavenwoodContext(String packageName, HandlerThread mainThread) {
+ public RavenwoodContext(String packageName, HandlerThread mainThread,
+ Supplier<Resources> resourcesSupplier) throws IOException {
mPackageName = packageName;
mMainThread = mainThread;
+ mResourcesSupplier = resourcesSupplier;
+ mFilesDir = createTempDir(packageName + "_files-dir");
+ mCacheDir = createTempDir(packageName + "_cache-dir");
// Services provided by a typical shipping device
registerService(ClipboardManager.class,
@@ -85,6 +113,11 @@
}
}
+ void cleanUp() {
+ deleteDir(mFilesDir);
+ deleteDir(mCacheDir);
+ }
+
@Override
public String getSystemServiceName(Class<?> serviceClass) {
// TODO: pivot to using SystemServiceRegistry
@@ -100,34 +133,35 @@
@Override
public Looper getMainLooper() {
Objects.requireNonNull(mMainThread,
- "Test must request setProvideMainThread() via RavenwoodRule");
+ "Test must request setProvideMainThread() via RavenwoodConfig");
return mMainThread.getLooper();
}
@Override
public Handler getMainThreadHandler() {
Objects.requireNonNull(mMainThread,
- "Test must request setProvideMainThread() via RavenwoodRule");
+ "Test must request setProvideMainThread() via RavenwoodConfig");
return mMainThread.getThreadHandler();
}
@Override
public Executor getMainExecutor() {
Objects.requireNonNull(mMainThread,
- "Test must request setProvideMainThread() via RavenwoodRule");
+ "Test must request setProvideMainThread() via RavenwoodConfig");
return mMainThread.getThreadExecutor();
}
@Override
public String getPackageName() {
return Objects.requireNonNull(mPackageName,
- "Test must request setPackageName() via RavenwoodRule");
+ "Test must request setPackageName() (or setTargetPackageName())"
+ + " via RavenwoodConfig");
}
@Override
public String getOpPackageName() {
return Objects.requireNonNull(mPackageName,
- "Test must request setPackageName() via RavenwoodRule");
+ "Test must request setPackageName() via RavenwoodConfig");
}
@Override
@@ -150,6 +184,61 @@
return Context.DEVICE_ID_DEFAULT;
}
+ @Override
+ public File getFilesDir() {
+ return mFilesDir;
+ }
+
+ @Override
+ public File getCacheDir() {
+ return mCacheDir;
+ }
+
+ @Override
+ public boolean deleteFile(String name) {
+ File f = new File(name);
+ return f.delete();
+ }
+
+ @Override
+ public Resources getResources() {
+ synchronized (mLock) {
+ if (mResources == null) {
+ mResources = mResourcesSupplier.get();
+ }
+ return mResources;
+ }
+ }
+
+ @Override
+ public AssetManager getAssets() {
+ return getResources().getAssets();
+ }
+
+ @Override
+ public Theme getTheme() {
+ synchronized (mLock) {
+ if (mTheme == null) {
+ mTheme = getResources().newTheme();
+ }
+ return mTheme;
+ }
+ }
+
+ @Override
+ public String getPackageResourcePath() {
+ return new File(RAVENWOOD_RESOURCE_APK).getAbsolutePath();
+ }
+
+ public void setApplicationContext(RavenwoodContext appContext) {
+ mAppContext = appContext;
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return mAppContext;
+ }
+
/**
* Wrap the given {@link Supplier} to become memoized.
*
@@ -175,4 +264,26 @@
public interface ThrowingSupplier<T> {
T get() throws Exception;
}
+
+
+ static File createTempDir(String prefix) throws IOException {
+ // Create a temp file, delete it and recreate it as a directory.
+ final File dir = File.createTempFile(prefix + "-", "");
+ dir.delete();
+ dir.mkdirs();
+ return dir;
+ }
+
+ static void deleteDir(File dir) {
+ File[] children = dir.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ if (child.isDirectory()) {
+ deleteDir(child);
+ } else {
+ child.delete();
+ }
+ }
+ }
+ }
}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodEnablementChecker.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodEnablementChecker.java
new file mode 100644
index 0000000..77275c4
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodEnablementChecker.java
@@ -0,0 +1,117 @@
+/*
+ * 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 android.platform.test.ravenwood;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.annotations.EnabledOnRavenwood;
+import android.platform.test.annotations.IgnoreUnderRavenwood;
+
+import org.junit.runner.Description;
+
+/**
+ * Calculates which tests need to be executed on Ravenwood.
+ */
+public class RavenwoodEnablementChecker {
+ private static final String TAG = "RavenwoodDisablementChecker";
+
+ private RavenwoodEnablementChecker() {
+ }
+
+ /**
+ * Determine if the given {@link Description} should be enabled when running on the
+ * Ravenwood test environment.
+ *
+ * A more specific method-level annotation always takes precedence over any class-level
+ * annotation, and an {@link EnabledOnRavenwood} annotation always takes precedence over
+ * an {@link DisabledOnRavenwood} annotation.
+ */
+ public static boolean shouldEnableOnRavenwood(Description description,
+ boolean takeIntoAccountRunDisabledTestsFlag) {
+ // First, consult any method-level annotations
+ if (description.isTest()) {
+ Boolean result = null;
+
+ // Stopgap for http://g/ravenwood/EPAD-N5ntxM
+ if (description.getMethodName().endsWith("$noRavenwood")) {
+ result = false;
+ } else if (description.getAnnotation(EnabledOnRavenwood.class) != null) {
+ result = true;
+ } else if (description.getAnnotation(DisabledOnRavenwood.class) != null) {
+ result = false;
+ } else if (description.getAnnotation(IgnoreUnderRavenwood.class) != null) {
+ result = false;
+ }
+ if (result != null) {
+ if (takeIntoAccountRunDisabledTestsFlag
+ && RavenwoodRule.private$ravenwood().isRunningDisabledTests()) {
+ result = !shouldStillIgnoreInProbeIgnoreMode(
+ description.getTestClass(), description.getMethodName());
+ }
+ }
+ if (result != null) {
+ return result;
+ }
+ }
+
+ // Otherwise, consult any class-level annotations
+ return shouldRunClassOnRavenwood(description.getTestClass(),
+ takeIntoAccountRunDisabledTestsFlag);
+ }
+
+ public static boolean shouldRunClassOnRavenwood(@NonNull Class<?> testClass,
+ boolean takeIntoAccountRunDisabledTestsFlag) {
+ boolean result = true;
+ if (testClass.getAnnotation(EnabledOnRavenwood.class) != null) {
+ result = true;
+ } else if (testClass.getAnnotation(DisabledOnRavenwood.class) != null) {
+ result = false;
+ } else if (testClass.getAnnotation(IgnoreUnderRavenwood.class) != null) {
+ result = false;
+ }
+ if (!result) {
+ if (takeIntoAccountRunDisabledTestsFlag
+ && RavenwoodRule.private$ravenwood().isRunningDisabledTests()) {
+ result = !shouldStillIgnoreInProbeIgnoreMode(testClass, null);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Check if a test should _still_ disabled even if {@code RUN_DISABLED_TESTS}
+ * is true, using {@code REALLY_DISABLED_PATTERN}.
+ *
+ * This only works on tests, not on classes.
+ */
+ static boolean shouldStillIgnoreInProbeIgnoreMode(
+ @NonNull Class<?> testClass, @Nullable String methodName) {
+ if (RavenwoodRule.private$ravenwood().getReallyDisabledPattern().pattern().isEmpty()) {
+ return false;
+ }
+
+ final var fullname = testClass.getName() + (methodName != null ? "#" + methodName : "");
+
+ System.out.println("XXX=" + fullname);
+
+ if (RavenwoodRule.private$ravenwood().getReallyDisabledPattern().matcher(fullname).find()) {
+ System.out.println("Still ignoring " + fullname);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java
new file mode 100644
index 0000000..e548611
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java
@@ -0,0 +1,134 @@
+/*
+ * 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 android.platform.test.ravenwood;
+
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * We use this class to load libandroid_runtime.
+ * In the future, we may load other native libraries.
+ */
+public final class RavenwoodNativeLoader {
+ public static final String CORE_NATIVE_CLASSES = "core_native_classes";
+ public static final String ICU_DATA_PATH = "icu.data.path";
+ public static final String KEYBOARD_PATHS = "keyboard_paths";
+ public static final String GRAPHICS_NATIVE_CLASSES = "graphics_native_classes";
+
+ public static final String LIBANDROID_RUNTIME_NAME = "android_runtime";
+
+ /**
+ * Classes with native methods that are backed by libandroid_runtime.
+ *
+ * See frameworks/base/core/jni/platform/host/HostRuntime.cpp
+ */
+ private static final Class<?>[] sLibandroidClasses = {
+ android.util.Log.class,
+ android.os.Parcel.class,
+ android.os.Binder.class,
+ android.content.res.ApkAssets.class,
+ android.content.res.AssetManager.class,
+ android.content.res.StringBlock.class,
+ android.content.res.XmlBlock.class,
+ };
+
+ /**
+ * Classes with native methods that are backed by libhwui.
+ *
+ * See frameworks/base/libs/hwui/apex/LayoutlibLoader.cpp
+ */
+ private static final Class<?>[] sLibhwuiClasses = {
+ android.graphics.Interpolator.class,
+ android.graphics.Matrix.class,
+ android.graphics.Path.class,
+ android.graphics.Color.class,
+ android.graphics.ColorSpace.class,
+ };
+
+ /**
+ * Extra strings needed to pass to register_android_graphics_classes().
+ *
+ * `android.graphics.Graphics` is not actually a class, so we just hardcode it here.
+ */
+ public final static String[] GRAPHICS_EXTRA_INIT_PARAMS = new String[] {
+ "android.graphics.Graphics"
+ };
+
+ private RavenwoodNativeLoader() {
+ }
+
+ private static void log(String message) {
+ System.out.println("RavenwoodNativeLoader: " + message);
+ }
+
+ private static void log(String fmt, Object... args) {
+ log(String.format(fmt, args));
+ }
+
+ private static void ensurePropertyNotSet(String key) {
+ if (System.getProperty(key) != null) {
+ throw new RuntimeException("System property \"" + key + "\" is set unexpectedly");
+ }
+ }
+
+ private static void setProperty(String key, String value) {
+ System.setProperty(key, value);
+ log("Property set: %s=\"%s\"", key, value);
+ }
+
+ private static void dumpSystemProperties() {
+ for (var prop : System.getProperties().entrySet()) {
+ log(" %s=\"%s\"", prop.getKey(), prop.getValue());
+ }
+ }
+
+ /**
+ * libandroid_runtime uses Java's system properties to decide what JNI methods to set up.
+ * Set up these properties and load the native library
+ */
+ public static void loadFrameworkNativeCode() {
+ if ("1".equals(System.getenv("RAVENWOOD_DUMP_PROPERTIES"))) {
+ log("Java system properties:");
+ dumpSystemProperties();
+ }
+
+ // Make sure these properties are not set.
+ ensurePropertyNotSet(CORE_NATIVE_CLASSES);
+ ensurePropertyNotSet(ICU_DATA_PATH);
+ ensurePropertyNotSet(KEYBOARD_PATHS);
+ ensurePropertyNotSet(GRAPHICS_NATIVE_CLASSES);
+
+ // Build the property values
+ final var joiner = Collectors.joining(",");
+ final var libandroidClasses =
+ Arrays.stream(sLibandroidClasses).map(Class::getName).collect(joiner);
+ final var libhwuiClasses = Stream.concat(
+ Arrays.stream(sLibhwuiClasses).map(Class::getName),
+ Arrays.stream(GRAPHICS_EXTRA_INIT_PARAMS)
+ ).collect(joiner);
+
+ // Load the libraries
+ setProperty(CORE_NATIVE_CLASSES, libandroidClasses);
+ setProperty(GRAPHICS_NATIVE_CLASSES, libhwuiClasses);
+ log("Loading " + LIBANDROID_RUNTIME_NAME + " for '" + libandroidClasses + "' and '"
+ + libhwuiClasses + "'");
+ RavenwoodCommonUtils.loadJniLibrary(LIBANDROID_RUNTIME_NAME);
+ }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
deleted file mode 100644
index 4357f2b..0000000
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * Copyright (C) 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 android.platform.test.ravenwood;
-
-import static org.junit.Assert.assertFalse;
-
-import android.app.ActivityManager;
-import android.app.Instrumentation;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.ServiceManager;
-import android.util.Log;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.internal.os.RuntimeInit;
-import com.android.server.LocalServices;
-
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.Description;
-import org.junit.runner.RunWith;
-import org.junit.runners.model.Statement;
-
-import java.io.PrintStream;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
-public class RavenwoodRuleImpl {
- private static final String MAIN_THREAD_NAME = "RavenwoodMain";
-
- /**
- * When enabled, attempt to dump all thread stacks just before we hit the
- * overall Tradefed timeout, to aid in debugging deadlocks.
- */
- private static final boolean ENABLE_TIMEOUT_STACKS = false;
- private static final int TIMEOUT_MILLIS = 9_000;
-
- private static final ScheduledExecutorService sTimeoutExecutor =
- Executors.newScheduledThreadPool(1);
-
- private static ScheduledFuture<?> sPendingTimeout;
-
- /**
- * When enabled, attempt to detect uncaught exceptions from background threads.
- */
- private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION = false;
-
- /**
- * When set, an unhandled exception was discovered (typically on a background thread), and we
- * capture it here to ensure it's reported as a test failure.
- */
- private static final AtomicReference<Throwable> sPendingUncaughtException =
- new AtomicReference<>();
-
- private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler =
- (thread, throwable) -> {
- // Remember the first exception we discover
- sPendingUncaughtException.compareAndSet(null, throwable);
- };
-
- public static void init(RavenwoodRule rule) {
- if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
- maybeThrowPendingUncaughtException(false);
- Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler);
- }
-
- RuntimeInit.redirectLogStreams();
-
- android.os.Process.init$ravenwood(rule.mUid, rule.mPid);
- android.os.Binder.init$ravenwood();
-// android.os.SystemProperties.init$ravenwood(
-// rule.mSystemProperties.getValues(),
-// rule.mSystemProperties.getKeyReadablePredicate(),
-// rule.mSystemProperties.getKeyWritablePredicate());
- setSystemProperties(rule.mSystemProperties);
-
- ServiceManager.init$ravenwood();
- LocalServices.removeAllServicesForTest();
-
- ActivityManager.init$ravenwood(rule.mCurrentUser);
-
- final HandlerThread main;
- if (rule.mProvideMainThread) {
- main = new HandlerThread(MAIN_THREAD_NAME);
- main.start();
- Looper.setMainLooperForTest(main.getLooper());
- } else {
- main = null;
- }
-
- rule.mContext = new RavenwoodContext(rule.mPackageName, main);
- rule.mInstrumentation = new Instrumentation();
- rule.mInstrumentation.basicInit(rule.mContext);
- InstrumentationRegistry.registerInstance(rule.mInstrumentation, Bundle.EMPTY);
-
- RavenwoodSystemServer.init(rule);
-
- if (ENABLE_TIMEOUT_STACKS) {
- sPendingTimeout = sTimeoutExecutor.schedule(RavenwoodRuleImpl::dumpStacks,
- TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
- }
-
- // Touch some references early to ensure they're <clinit>'ed
- Objects.requireNonNull(Build.TYPE);
- Objects.requireNonNull(Build.VERSION.SDK);
- }
-
- public static void reset(RavenwoodRule rule) {
- if (ENABLE_TIMEOUT_STACKS) {
- sPendingTimeout.cancel(false);
- }
-
- RavenwoodSystemServer.reset(rule);
-
- InstrumentationRegistry.registerInstance(null, Bundle.EMPTY);
- rule.mInstrumentation = null;
- rule.mContext = null;
-
- if (rule.mProvideMainThread) {
- Looper.getMainLooper().quit();
- Looper.clearMainLooperForTest();
- }
-
- ActivityManager.reset$ravenwood();
-
- LocalServices.removeAllServicesForTest();
- ServiceManager.reset$ravenwood();
-
- setSystemProperties(RavenwoodSystemProperties.DEFAULT_VALUES);
- android.os.Binder.reset$ravenwood();
- android.os.Process.reset$ravenwood();
-
- if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
- maybeThrowPendingUncaughtException(true);
- }
- }
-
- public static void logTestRunner(String label, Description description) {
- // This message string carefully matches the exact format emitted by on-device tests, to
- // aid developers in debugging raw text logs
- Log.e("TestRunner", label + ": " + description.getMethodName()
- + "(" + description.getTestClass().getName() + ")");
- }
-
- private static void dumpStacks() {
- final PrintStream out = System.err;
- out.println("-----BEGIN ALL THREAD STACKS-----");
- final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
- for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) {
- out.println();
- Thread t = stack.getKey();
- out.println(t.toString() + " ID=" + t.getId());
- for (StackTraceElement e : stack.getValue()) {
- out.println("\tat " + e);
- }
- }
- out.println("-----END ALL THREAD STACKS-----");
- }
-
- /**
- * If there's a pending uncaught exception, consume and throw it now. Typically used to
- * report an exception on a background thread as a failure for the currently running test.
- */
- private static void maybeThrowPendingUncaughtException(boolean duringReset) {
- final Throwable pending = sPendingUncaughtException.getAndSet(null);
- if (pending != null) {
- if (duringReset) {
- throw new IllegalStateException(
- "Found an uncaught exception during this test", pending);
- } else {
- throw new IllegalStateException(
- "Found an uncaught exception before this test started", pending);
- }
- }
- }
-
- public static void validate(Statement base, Description description,
- boolean enableOptionalValidation) {
- validateTestRunner(base, description, enableOptionalValidation);
- validateTestAnnotations(base, description, enableOptionalValidation);
- }
-
- private static void validateTestRunner(Statement base, Description description,
- boolean shouldFail) {
- final var testClass = description.getTestClass();
- final var runWith = testClass.getAnnotation(RunWith.class);
- if (runWith == null) {
- return;
- }
-
- // Due to build dependencies, we can't directly refer to androidx classes here,
- // so just check the class name instead.
- if (runWith.value().getCanonicalName().equals("androidx.test.runner.AndroidJUnit4")) {
- var message = "Test " + testClass.getCanonicalName() + " uses deprecated"
- + " test runner androidx.test.runner.AndroidJUnit4."
- + " Switch to androidx.test.ext.junit.runners.AndroidJUnit4.";
- if (shouldFail) {
- Assert.fail(message);
- } else {
- System.err.println("Warning: " + message);
- }
- }
- }
-
- /**
- * @return if a method has any of annotations.
- */
- private static boolean hasAnyAnnotations(Method m, Class<? extends Annotation>... annotations) {
- for (var anno : annotations) {
- if (m.getAnnotation(anno) != null) {
- return true;
- }
- }
- return false;
- }
-
- private static void validateTestAnnotations(Statement base, Description description,
- boolean enableOptionalValidation) {
- final var testClass = description.getTestClass();
-
- final var message = new StringBuilder();
-
- boolean hasErrors = false;
- for (Method m : collectMethods(testClass)) {
- if (Modifier.isPublic(m.getModifiers()) && m.getName().startsWith("test")) {
- if (!hasAnyAnnotations(m, Test.class, Before.class, After.class,
- BeforeClass.class, AfterClass.class)) {
- message.append("\nMethod " + m.getName() + "() doesn't have @Test");
- hasErrors = true;
- }
- }
- if ("setUp".equals(m.getName())) {
- if (!hasAnyAnnotations(m, Before.class)) {
- message.append("\nMethod " + m.getName() + "() doesn't have @Before");
- hasErrors = true;
- }
- if (!Modifier.isPublic(m.getModifiers())) {
- message.append("\nMethod " + m.getName() + "() must be public");
- hasErrors = true;
- }
- }
- if ("tearDown".equals(m.getName())) {
- if (!hasAnyAnnotations(m, After.class)) {
- message.append("\nMethod " + m.getName() + "() doesn't have @After");
- hasErrors = true;
- }
- if (!Modifier.isPublic(m.getModifiers())) {
- message.append("\nMethod " + m.getName() + "() must be public");
- hasErrors = true;
- }
- }
- }
- assertFalse("Problem(s) detected in class " + testClass.getCanonicalName() + ":"
- + message, hasErrors);
- }
-
- /**
- * Collect all (public or private or any) methods in a class, including inherited methods.
- */
- private static List<Method> collectMethods(Class<?> clazz) {
- var ret = new ArrayList<Method>();
- collectMethods(clazz, ret);
- return ret;
- }
-
- private static void collectMethods(Class<?> clazz, List<Method> result) {
- // Class.getMethods() only return public methods, so we need to use getDeclaredMethods()
- // instead, and recurse.
- for (var m : clazz.getDeclaredMethods()) {
- result.add(m);
- }
- if (clazz.getSuperclass() != null) {
- collectMethods(clazz.getSuperclass(), result);
- }
- }
-
- /**
- * Set the current configuration to the actual SystemProperties.
- */
- public static void setSystemProperties(RavenwoodSystemProperties ravenwoodSystemProperties) {
- var clone = new RavenwoodSystemProperties(ravenwoodSystemProperties, true);
-
- android.os.SystemProperties.init$ravenwood(
- clone.getValues(),
- clone.getKeyReadablePredicate(),
- clone.getKeyWritablePredicate());
- }
-}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
new file mode 100644
index 0000000..03513ab
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
@@ -0,0 +1,249 @@
+/*
+ * 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 android.platform.test.ravenwood;
+
+import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicMember;
+
+import static org.junit.Assert.fail;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.ravenwood.common.RavenwoodRuntimeException;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.WeakHashMap;
+
+/**
+ * Used to store various states associated with the current test runner that's inly needed
+ * in junit-impl.
+ *
+ * We don't want to put it in junit-src to avoid having to recompile all the downstream
+ * dependencies after changing this class.
+ *
+ * All members must be called from the runner's main thread.
+ */
+public final class RavenwoodRunnerState {
+ private static final String TAG = "RavenwoodRunnerState";
+
+ @GuardedBy("sStates")
+ private static final WeakHashMap<RavenwoodAwareTestRunner, RavenwoodRunnerState> sStates =
+ new WeakHashMap<>();
+
+ private final RavenwoodAwareTestRunner mRunner;
+
+ /**
+ * Ctor.
+ */
+ public RavenwoodRunnerState(RavenwoodAwareTestRunner runner) {
+ mRunner = runner;
+ }
+
+ private Description mClassDescription;
+ private Description mMethodDescription;
+
+ private RavenwoodConfig mCurrentConfig;
+ private RavenwoodRule mCurrentRule;
+ private boolean mHasRavenwoodRule;
+
+ public Description getClassDescription() {
+ return mClassDescription;
+ }
+
+ public void enterTestClass(Description classDescription) throws IOException {
+ mClassDescription = classDescription;
+
+ mHasRavenwoodRule = hasRavenwoodRule(mRunner.getTestClass().getJavaClass());
+ mCurrentConfig = extractConfiguration(mRunner.getTestClass().getJavaClass());
+
+ if (mCurrentConfig != null) {
+ RavenwoodRuntimeEnvironmentController.init(mCurrentConfig);
+ }
+ }
+
+ public void exitTestClass() {
+ if (mCurrentConfig != null) {
+ try {
+ RavenwoodRuntimeEnvironmentController.reset();
+ } finally {
+ mClassDescription = null;
+ }
+ }
+ }
+
+ public void enterTestMethod(Description description) {
+ mMethodDescription = description;
+ }
+
+ public void exitTestMethod() {
+ mMethodDescription = null;
+ }
+
+ public void enterRavenwoodRule(RavenwoodRule rule) throws IOException {
+ if (!mHasRavenwoodRule) {
+ fail("If you have a RavenwoodRule in your test, make sure the field type is"
+ + " RavenwoodRule so Ravenwood can detect it.");
+ }
+ if (mCurrentConfig != null) {
+ fail("RavenwoodConfig and RavenwoodRule cannot be used in the same class."
+ + " Suggest migrating to RavenwoodConfig.");
+ }
+ if (mCurrentRule != null) {
+ fail("Multiple nesting RavenwoodRule's are detected in the same class,"
+ + " which is not supported.");
+ }
+ mCurrentRule = rule;
+ RavenwoodRuntimeEnvironmentController.init(rule.getConfiguration());
+ }
+
+ public void exitRavenwoodRule(RavenwoodRule rule) {
+ if (mCurrentRule != rule) {
+ return; // This happens if the rule did _not_ take effect somehow.
+ }
+
+ try {
+ RavenwoodRuntimeEnvironmentController.reset();
+ } finally {
+ mCurrentRule = null;
+ }
+ }
+
+ /**
+ * @return a configuration from a test class, if any.
+ */
+ @Nullable
+ private RavenwoodConfig extractConfiguration(Class<?> testClass) {
+ var field = findConfigurationField(testClass);
+ if (field == null) {
+ if (mHasRavenwoodRule) {
+ // Should be handled by RavenwoodRule
+ return null;
+ }
+
+ // If no RavenwoodConfig and no RavenwoodRule, return a default config
+ return new RavenwoodConfig.Builder().build();
+ }
+ if (mHasRavenwoodRule) {
+ fail("RavenwoodConfig and RavenwoodRule cannot be used in the same class."
+ + " Suggest migrating to RavenwoodConfig.");
+ }
+
+ try {
+ return (RavenwoodConfig) field.get(null);
+ } catch (IllegalAccessException e) {
+ throw new RavenwoodRuntimeException("Failed to fetch from the configuration field", e);
+ }
+ }
+
+ /**
+ * @return true if the current target class (or its super classes) has any @Rule / @ClassRule
+ * fields of type RavenwoodRule.
+ *
+ * Note, this check won't detect cases where a Rule is of type
+ * {@link TestRule} and still be a {@link RavenwoodRule}. But that'll be detected at runtime
+ * as a failure, in {@link #enterRavenwoodRule}.
+ */
+ private static boolean hasRavenwoodRule(Class<?> testClass) {
+ for (var field : testClass.getDeclaredFields()) {
+ if (!field.isAnnotationPresent(Rule.class)
+ && !field.isAnnotationPresent(ClassRule.class)) {
+ continue;
+ }
+ if (field.getType().equals(RavenwoodRule.class)) {
+ return true;
+ }
+ }
+ // JUnit supports rules as methods, so we need to check them too.
+ for (var method : testClass.getDeclaredMethods()) {
+ if (!method.isAnnotationPresent(Rule.class)
+ && !method.isAnnotationPresent(ClassRule.class)) {
+ continue;
+ }
+ if (method.getReturnType().equals(RavenwoodRule.class)) {
+ return true;
+ }
+ }
+ // Look into the super class.
+ if (!testClass.getSuperclass().equals(Object.class)) {
+ return hasRavenwoodRule(testClass.getSuperclass());
+ }
+ return false;
+ }
+
+ /**
+ * Find and return a field with @RavenwoodConfig.Config, which must be of type
+ * RavenwoodConfig.
+ */
+ @Nullable
+ private static Field findConfigurationField(Class<?> testClass) {
+ Field foundField = null;
+
+ for (var field : testClass.getDeclaredFields()) {
+ final var hasAnot = field.isAnnotationPresent(RavenwoodConfig.Config.class);
+ final var isType = field.getType().equals(RavenwoodConfig.class);
+
+ if (hasAnot) {
+ if (isType) {
+ // Good, use this field.
+ if (foundField != null) {
+ fail(String.format(
+ "Class %s has multiple fields with %s",
+ testClass.getCanonicalName(),
+ "@RavenwoodConfig.Config"));
+ }
+ // Make sure it's static public
+ ensureIsPublicMember(field, true);
+
+ foundField = field;
+ } else {
+ fail(String.format(
+ "Field %s.%s has %s but type is not %s",
+ testClass.getCanonicalName(),
+ field.getName(),
+ "@RavenwoodConfig.Config",
+ "RavenwoodConfig"));
+ return null; // unreachable
+ }
+ } else {
+ if (isType) {
+ fail(String.format(
+ "Field %s.%s does not have %s but type is %s",
+ testClass.getCanonicalName(),
+ field.getName(),
+ "@RavenwoodConfig.Config",
+ "RavenwoodConfig"));
+ return null; // unreachable
+ } else {
+ // Unrelated field, ignore.
+ continue;
+ }
+ }
+ }
+ if (foundField != null) {
+ return foundField;
+ }
+ if (!testClass.getSuperclass().equals(Object.class)) {
+ return findConfigurationField(testClass.getSuperclass());
+ }
+ return null;
+ }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
new file mode 100644
index 0000000..40b14db
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 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 android.platform.test.ravenwood;
+
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_INST_RESOURCE_APK;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RESOURCE_APK;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERSION_JAVA_SYSPROP;
+
+import android.app.ActivityManager;
+import android.app.Instrumentation;
+import android.app.ResourcesManager;
+import android.content.res.Resources;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.ServiceManager;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.os.RuntimeInit;
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+import com.android.ravenwood.common.RavenwoodRuntimeException;
+import com.android.ravenwood.common.SneakyThrow;
+import com.android.server.LocalServices;
+
+import org.junit.runner.Description;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+/**
+ * Responsible for initializing and de-initializing the environment, according to a
+ * {@link RavenwoodConfig}.
+ */
+public class RavenwoodRuntimeEnvironmentController {
+ private static final String TAG = "RavenwoodRuntimeEnvironmentController";
+
+ private RavenwoodRuntimeEnvironmentController() {
+ }
+
+ private static final String MAIN_THREAD_NAME = "RavenwoodMain";
+
+ /**
+ * When enabled, attempt to dump all thread stacks just before we hit the
+ * overall Tradefed timeout, to aid in debugging deadlocks.
+ */
+ private static final boolean ENABLE_TIMEOUT_STACKS =
+ "1".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));
+
+ private static final int TIMEOUT_MILLIS = 9_000;
+
+ private static final ScheduledExecutorService sTimeoutExecutor =
+ Executors.newScheduledThreadPool(1);
+
+ private static ScheduledFuture<?> sPendingTimeout;
+
+ private static long sOriginalIdentityToken = -1;
+
+ /**
+ * When enabled, attempt to detect uncaught exceptions from background threads.
+ */
+ private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION =
+ "1".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));
+
+ /**
+ * When set, an unhandled exception was discovered (typically on a background thread), and we
+ * capture it here to ensure it's reported as a test failure.
+ */
+ private static final AtomicReference<Throwable> sPendingUncaughtException =
+ new AtomicReference<>();
+
+ private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler =
+ (thread, throwable) -> {
+ // Remember the first exception we discover
+ sPendingUncaughtException.compareAndSet(null, throwable);
+ };
+
+ // TODO: expose packCallingIdentity function in libbinder and use it directly
+ // See: packCallingIdentity in frameworks/native/libs/binder/IPCThreadState.cpp
+ private static long packBinderIdentityToken(
+ boolean hasExplicitIdentity, int callingUid, int callingPid) {
+ long res = ((long) callingUid << 32) | callingPid;
+ if (hasExplicitIdentity) {
+ res |= (0x1 << 30);
+ } else {
+ res &= ~(0x1 << 30);
+ }
+ return res;
+ }
+
+ private static RavenwoodConfig sConfig;
+ private static boolean sInitialized = false;
+
+ /**
+ * Initialize the global environment.
+ */
+ public static void globalInitOnce() {
+ if (sInitialized) {
+ return;
+ }
+ sInitialized = true;
+
+ // We haven't initialized liblog yet, so directly write to System.out here.
+ RavenwoodCommonUtils.log(TAG, "globalInit()");
+
+ // Do the basic set up for the android sysprops.
+ setSystemProperties(RavenwoodSystemProperties.DEFAULT_VALUES);
+
+ // Make sure libandroid_runtime is loaded.
+ RavenwoodNativeLoader.loadFrameworkNativeCode();
+
+ // Redirect stdout/stdin to liblog.
+ RuntimeInit.redirectLogStreams();
+
+ if (RAVENWOOD_VERBOSE_LOGGING) {
+ RavenwoodCommonUtils.log(TAG, "Force enabling verbose logging");
+ try {
+ Os.setenv("ANDROID_LOG_TAGS", "*:v", true);
+ } catch (ErrnoException e) {
+ // Shouldn't happen.
+ }
+ }
+
+ System.setProperty(RAVENWOOD_VERSION_JAVA_SYSPROP, "1");
+ // This will let AndroidJUnit4 use the original runner.
+ System.setProperty("android.junit.runner",
+ "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner");
+ }
+
+ /**
+ * Initialize the environment.
+ */
+ public static void init(RavenwoodConfig config) throws IOException {
+ if (RAVENWOOD_VERBOSE_LOGGING) {
+ Log.i(TAG, "init() called here", new RuntimeException("STACKTRACE"));
+ }
+ try {
+ initInner(config);
+ } catch (Exception th) {
+ Log.e(TAG, "init() failed", th);
+ reset();
+ SneakyThrow.sneakyThrow(th);
+ }
+ }
+
+ private static void initInner(RavenwoodConfig config) throws IOException {
+ if (sConfig != null) {
+ throw new RavenwoodRuntimeException("Internal error: init() called without reset()");
+ }
+ sConfig = config;
+ if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
+ maybeThrowPendingUncaughtException(false);
+ Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler);
+ }
+
+ android.os.Process.init$ravenwood(config.mUid, config.mPid);
+ sOriginalIdentityToken = Binder.clearCallingIdentity();
+ Binder.restoreCallingIdentity(packBinderIdentityToken(false, config.mUid, config.mPid));
+ setSystemProperties(config.mSystemProperties);
+
+ ServiceManager.init$ravenwood();
+ LocalServices.removeAllServicesForTest();
+
+ ActivityManager.init$ravenwood(config.mCurrentUser);
+
+ final HandlerThread main;
+ if (config.mProvideMainThread) {
+ main = new HandlerThread(MAIN_THREAD_NAME);
+ main.start();
+ Looper.setMainLooperForTest(main.getLooper());
+ } else {
+ main = null;
+ }
+
+ final boolean isSelfInstrumenting =
+ Objects.equals(config.mTestPackageName, config.mTargetPackageName);
+
+ // This will load the resources from the apk set to `resource_apk` in the build file.
+ // This is supposed to be the "target app"'s resources.
+ final Supplier<Resources> targetResourcesLoader = () -> {
+ var file = new File(RAVENWOOD_RESOURCE_APK);
+ return config.mState.loadResources(file.exists() ? file : null);
+ };
+
+ // Set up test context's (== instrumentation context's) resources.
+ // If the target package name == test package name, then we use the main resources.
+ final Supplier<Resources> instResourcesLoader;
+ if (isSelfInstrumenting) {
+ instResourcesLoader = targetResourcesLoader;
+ } else {
+ instResourcesLoader = () -> {
+ var file = new File(RAVENWOOD_INST_RESOURCE_APK);
+ return config.mState.loadResources(file.exists() ? file : null);
+ };
+ }
+
+ var instContext = new RavenwoodContext(
+ config.mTestPackageName, main, instResourcesLoader);
+ var targetContext = new RavenwoodContext(
+ config.mTargetPackageName, main, targetResourcesLoader);
+
+ // Set up app context.
+ var appContext = new RavenwoodContext(
+ config.mTargetPackageName, main, targetResourcesLoader);
+ appContext.setApplicationContext(appContext);
+ if (isSelfInstrumenting) {
+ instContext.setApplicationContext(appContext);
+ targetContext.setApplicationContext(appContext);
+ } else {
+ // When instrumenting into another APK, the test context doesn't have an app context.
+ targetContext.setApplicationContext(appContext);
+ }
+ config.mInstContext = instContext;
+ config.mTargetContext = targetContext;
+
+ // Prepare other fields.
+ config.mInstrumentation = new Instrumentation();
+ config.mInstrumentation.basicInit(config.mInstContext, config.mTargetContext);
+ InstrumentationRegistry.registerInstance(config.mInstrumentation, Bundle.EMPTY);
+
+ RavenwoodSystemServer.init(config);
+
+ if (ENABLE_TIMEOUT_STACKS) {
+ sPendingTimeout = sTimeoutExecutor.schedule(
+ RavenwoodRuntimeEnvironmentController::dumpStacks,
+ TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ }
+
+ // Touch some references early to ensure they're <clinit>'ed
+ Objects.requireNonNull(Build.TYPE);
+ Objects.requireNonNull(Build.VERSION.SDK);
+ }
+
+ /**
+ * De-initialize.
+ */
+ public static void reset() {
+ if (RAVENWOOD_VERBOSE_LOGGING) {
+ Log.i(TAG, "reset() called here", new RuntimeException("STACKTRACE"));
+ }
+ if (sConfig == null) {
+ throw new RavenwoodRuntimeException("Internal error: reset() already called");
+ }
+ var config = sConfig;
+ sConfig = null;
+
+ if (ENABLE_TIMEOUT_STACKS) {
+ sPendingTimeout.cancel(false);
+ }
+
+ RavenwoodSystemServer.reset(config);
+
+ InstrumentationRegistry.registerInstance(null, Bundle.EMPTY);
+ config.mInstrumentation = null;
+ if (config.mInstContext != null) {
+ ((RavenwoodContext) config.mInstContext).cleanUp();
+ }
+ if (config.mTargetContext != null) {
+ ((RavenwoodContext) config.mTargetContext).cleanUp();
+ }
+ config.mInstContext = null;
+ config.mTargetContext = null;
+
+ if (config.mProvideMainThread) {
+ Looper.getMainLooper().quit();
+ Looper.clearMainLooperForTest();
+ }
+
+ ActivityManager.reset$ravenwood();
+
+ LocalServices.removeAllServicesForTest();
+ ServiceManager.reset$ravenwood();
+
+ setSystemProperties(RavenwoodSystemProperties.DEFAULT_VALUES);
+ if (sOriginalIdentityToken != -1) {
+ Binder.restoreCallingIdentity(sOriginalIdentityToken);
+ }
+ android.os.Process.reset$ravenwood();
+
+ try {
+ ResourcesManager.setInstance(null); // Better structure needed.
+ } catch (Exception e) {
+ // AOSP-CHANGE: AOSP doesn't support resources yet.
+ }
+
+ if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
+ maybeThrowPendingUncaughtException(true);
+ }
+ }
+
+ public static void logTestRunner(String label, Description description) {
+ // This message string carefully matches the exact format emitted by on-device tests, to
+ // aid developers in debugging raw text logs
+ Log.e("TestRunner", label + ": " + description.getMethodName()
+ + "(" + description.getTestClass().getName() + ")");
+ }
+
+ private static void dumpStacks() {
+ final PrintStream out = System.err;
+ out.println("-----BEGIN ALL THREAD STACKS-----");
+ final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
+ for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) {
+ out.println();
+ Thread t = stack.getKey();
+ out.println(t.toString() + " ID=" + t.getId());
+ for (StackTraceElement e : stack.getValue()) {
+ out.println("\tat " + e);
+ }
+ }
+ out.println("-----END ALL THREAD STACKS-----");
+ }
+
+ /**
+ * If there's a pending uncaught exception, consume and throw it now. Typically used to
+ * report an exception on a background thread as a failure for the currently running test.
+ */
+ private static void maybeThrowPendingUncaughtException(boolean duringReset) {
+ final Throwable pending = sPendingUncaughtException.getAndSet(null);
+ if (pending != null) {
+ if (duringReset) {
+ throw new IllegalStateException(
+ "Found an uncaught exception during this test", pending);
+ } else {
+ throw new IllegalStateException(
+ "Found an uncaught exception before this test started", pending);
+ }
+ }
+ }
+
+ /**
+ * Set the current configuration to the actual SystemProperties.
+ */
+ public static void setSystemProperties(RavenwoodSystemProperties ravenwoodSystemProperties) {
+ var clone = new RavenwoodSystemProperties(ravenwoodSystemProperties, true);
+
+ android.os.SystemProperties.init$ravenwood(
+ clone.getValues(),
+ clone.getKeyReadablePredicate(),
+ clone.getKeyWritablePredicate());
+ }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
index cd6b61d..3946dd84 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
@@ -61,19 +61,19 @@
private static TimingsTraceAndSlog sTimings;
private static SystemServiceManager sServiceManager;
- public static void init(RavenwoodRule rule) {
+ public static void init(RavenwoodConfig config) {
// Avoid overhead if no services required
- if (rule.mServicesRequired.isEmpty()) return;
+ if (config.mServicesRequired.isEmpty()) return;
sStartedServices = new ArraySet<>();
sTimings = new TimingsTraceAndSlog();
- sServiceManager = new SystemServiceManager(rule.mContext);
+ sServiceManager = new SystemServiceManager(config.mInstContext);
sServiceManager.setStartInfo(false,
SystemClock.elapsedRealtime(),
SystemClock.uptimeMillis());
LocalServices.addService(SystemServiceManager.class, sServiceManager);
- startServices(rule.mServicesRequired);
+ startServices(config.mServicesRequired);
sServiceManager.sealStartedServices();
// TODO: expand to include additional boot phases when relevant
@@ -81,7 +81,7 @@
sServiceManager.startBootPhase(sTimings, SystemService.PHASE_BOOT_COMPLETED);
}
- public static void reset(RavenwoodRule rule) {
+ public static void reset(RavenwoodConfig config) {
// TODO: consider introducing shutdown boot phases
LocalServices.removeServiceForTest(SystemServiceManager.class);
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
new file mode 100644
index 0000000..428eb57
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
@@ -0,0 +1,191 @@
+/*
+ * 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 android.platform.test.ravenwood;
+
+import android.util.Log;
+
+import org.junit.runner.Description;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Collect test result stats and write them into a CSV file containing the test results.
+ *
+ * The output file is created as `/tmp/Ravenwood-stats_[TEST-MODULE=NAME]_[TIMESTAMP].csv`.
+ * A symlink to the latest result will be created as
+ * `/tmp/Ravenwood-stats_[TEST-MODULE=NAME]_latest.csv`.
+ */
+public class RavenwoodTestStats {
+ private static final String TAG = "RavenwoodTestStats";
+ private static final String HEADER = "Module,Class,ClassDesc,Passed,Failed,Skipped";
+
+ private static RavenwoodTestStats sInstance;
+
+ /**
+ * @return a singleton instance.
+ */
+ public static RavenwoodTestStats getInstance() {
+ if (sInstance == null) {
+ sInstance = new RavenwoodTestStats();
+ }
+ return sInstance;
+ }
+
+ /**
+ * Represents a test result.
+ */
+ public enum Result {
+ Passed,
+ Failed,
+ Skipped,
+ }
+
+ private final File mOutputFile;
+ private final PrintWriter mOutputWriter;
+ private final String mTestModuleName;
+
+ public final Map<Description, Map<Description, Result>> mStats = new HashMap<>();
+
+ /** Ctor */
+ public RavenwoodTestStats() {
+ mTestModuleName = guessTestModuleName();
+
+ var basename = "Ravenwood-stats_" + mTestModuleName + "_";
+
+ // Get the current time
+ LocalDateTime now = LocalDateTime.now();
+ DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
+
+ var tmpdir = System.getProperty("java.io.tmpdir");
+ mOutputFile = new File(tmpdir, basename + now.format(fmt) + ".csv");
+
+ try {
+ mOutputWriter = new PrintWriter(mOutputFile);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e);
+ }
+
+ // Crete the "latest" symlink.
+ Path symlink = Paths.get(tmpdir, basename + "latest.csv");
+ try {
+ if (Files.exists(symlink)) {
+ Files.delete(symlink);
+ }
+ Files.createSymbolicLink(symlink, Paths.get(mOutputFile.getName()));
+
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e);
+ }
+
+ Log.i(TAG, "Test result stats file: " + mOutputFile);
+
+ // Print the header.
+ mOutputWriter.println(HEADER);
+ mOutputWriter.flush();
+ }
+
+ private String guessTestModuleName() {
+ // Assume the current directory name is the test module name.
+ File cwd;
+ try {
+ cwd = new File(".").getCanonicalFile();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to get the current directory", e);
+ }
+ return cwd.getName();
+ }
+
+ private void addResult(Description classDescription, Description methodDescription,
+ Result result) {
+ mStats.compute(classDescription, (classDesc, value) -> {
+ if (value == null) {
+ value = new HashMap<>();
+ }
+ value.put(methodDescription, result);
+ return value;
+ });
+ }
+
+ /**
+ * Call it when a test class is skipped.
+ */
+ public void onClassSkipped(Description classDescription) {
+ addResult(classDescription, Description.EMPTY, Result.Skipped);
+ onClassFinished(classDescription);
+ }
+
+ /**
+ * Call it when a test method is finished.
+ */
+ public void onTestFinished(Description classDescription, Description testDescription,
+ Result result) {
+ addResult(classDescription, testDescription, result);
+ }
+
+ /**
+ * Call it when a test class is finished.
+ */
+ public void onClassFinished(Description classDescription) {
+ int passed = 0;
+ int skipped = 0;
+ int failed = 0;
+ var stats = mStats.get(classDescription);
+ if (stats == null) {
+ return;
+ }
+ for (var e : stats.values()) {
+ switch (e) {
+ case Passed: passed++; break;
+ case Skipped: skipped++; break;
+ case Failed: failed++; break;
+ }
+ }
+
+ var testClass = extractTestClass(classDescription);
+
+ mOutputWriter.printf("%s,%s,%s,%d,%d,%d\n",
+ mTestModuleName, (testClass == null ? "?" : testClass.getCanonicalName()),
+ classDescription, passed, failed, skipped);
+ mOutputWriter.flush();
+ }
+
+ /**
+ * Try to extract the class from a description, which is needed because
+ * ParameterizedAndroidJunit4's description doesn't contain a class.
+ */
+ private Class<?> extractTestClass(Description desc) {
+ if (desc.getTestClass() != null) {
+ return desc.getTestClass();
+ }
+ // Look into the children.
+ for (var child : desc.getChildren()) {
+ var fromChild = extractTestClass(child);
+ if (fromChild != null) {
+ return fromChild;
+ }
+ }
+ return null;
+ }
+}
diff --git a/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java b/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java
deleted file mode 100644
index 2fb8074..0000000
--- a/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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 android.platform.test.annotations;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Tests marked with this annotation are only executed when running on Ravenwood, but not
- * on a device.
- *
- * This is basically equivalent to the opposite of {@link DisabledOnRavenwood}, but in order to
- * avoid complex structure, and there's no equivalent to the opposite {@link EnabledOnRavenwood},
- * which means if a test class has this annotation, you can't negate it in subclasses or
- * on a per-method basis.
- *
- * THIS ANNOTATION CANNOT BE ADDED TO CLASSES AT THIS PONINT.
- * See {@link com.android.ravenwoodtest.bivalenttest.RavenwoodClassRuleRavenwoodOnlyTest}
- * for the reason.
- *
- * The {@code RAVENWOOD_RUN_DISABLED_TESTS} environmental variable won't work because it won't be
- * propagated to the device. (We may support it in the future, possibly using a debug. sysprop.)
- *
- * @hide
- */
-@Inherited
-@Target({ElementType.METHOD})
-@Retention(RetentionPolicy.RUNTIME)
-public @interface DisabledOnNonRavenwood {
- /**
- * General free-form description of why this test is being ignored.
- */
- String reason() default "";
-}
diff --git a/ravenwood/junit-src/android/platform/test/annotations/NoRavenizer.java b/ravenwood/junit-src/android/platform/test/annotations/NoRavenizer.java
new file mode 100644
index 0000000..a84f16f
--- /dev/null
+++ b/ravenwood/junit-src/android/platform/test/annotations/NoRavenizer.java
@@ -0,0 +1,36 @@
+/*
+ * 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 android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Disable the ravenizer preprocessor for a class. This should be only used for testing
+ * ravenizer itself, or to workaround issues with the preprocessor. A test class probably won't run
+ * properly if it's not preprocessed.
+ *
+ * @hide
+ */
+@Inherited
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface NoRavenizer {
+}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
new file mode 100644
index 0000000..5ba972d
--- /dev/null
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
@@ -0,0 +1,733 @@
+/*
+ * 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 android.platform.test.ravenwood;
+
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicVoidMethod;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.isOnRavenwood;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import org.junit.Assume;
+import org.junit.AssumptionViolatedException;
+import org.junit.internal.builders.AllDefaultPossibilitiesBuilder;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runner.Runner;
+import org.junit.runner.manipulation.Filter;
+import org.junit.runner.manipulation.Filterable;
+import org.junit.runner.manipulation.InvalidOrderingException;
+import org.junit.runner.manipulation.NoTestsRemainException;
+import org.junit.runner.manipulation.Orderable;
+import org.junit.runner.manipulation.Orderer;
+import org.junit.runner.manipulation.Sortable;
+import org.junit.runner.manipulation.Sorter;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runner.notification.StoppedByUserException;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.Suite;
+import org.junit.runners.model.MultipleFailureException;
+import org.junit.runners.model.RunnerBuilder;
+import org.junit.runners.model.Statement;
+import org.junit.runners.model.TestClass;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Stack;
+import java.util.function.BiConsumer;
+
+/**
+ * A test runner used for Ravenwood.
+ *
+ * It will delegate to another runner specified with {@link InnerRunner}
+ * (default = {@link BlockJUnit4ClassRunner}) with the following features.
+ * - Add a {@link RavenwoodAwareTestRunnerHook#onRunnerInitializing} hook, which is called before
+ * the inner runner gets a chance to run. This can be used to initialize stuff used by the
+ * inner runner.
+ * - Add hook points, which are handed by RavenwoodAwareTestRunnerHook, with help from
+ * the four test rules such as {@link #sImplicitClassOuterRule}, which are also injected by
+ * the ravenizer tool.
+ *
+ * We use this runner to:
+ * - Initialize the bare minimum environmnet just to be enough to make the actual test runners
+ * happy.
+ * - Handle {@link android.platform.test.annotations.DisabledOnRavenwood}.
+ *
+ * This class is built such that it can also be used on a real device, but in that case
+ * it will basically just delegate to the inner wrapper, and won't do anything special.
+ * (no hooks, etc.)
+ */
+public final class RavenwoodAwareTestRunner extends Runner implements Filterable, Orderable {
+ public static final String TAG = "Ravenwood";
+
+ @Inherited
+ @Target({TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface InnerRunner {
+ Class<? extends Runner> value();
+ }
+
+ /**
+ * An annotation similar to JUnit's BeforeClass, but this gets executed before
+ * the inner runner is instantiated, and only on Ravenwood.
+ * It can be used to initialize what's needed by the inner runner.
+ */
+ @Target({METHOD})
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface RavenwoodTestRunnerInitializing {
+ }
+
+ /** Scope of a hook. */
+ public enum Scope {
+ Class,
+ Instance,
+ }
+
+ /** Order of a hook. */
+ public enum Order {
+ Outer,
+ Inner,
+ }
+
+ // The following four rule instances will be injected to tests by the Ravenizer tool.
+ private static class RavenwoodClassOuterRule implements TestRule {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return getCurrentRunner().wrapWithHooks(base, description, Scope.Class, Order.Outer);
+ }
+ }
+
+ private static class RavenwoodClassInnerRule implements TestRule {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return getCurrentRunner().wrapWithHooks(base, description, Scope.Class, Order.Inner);
+ }
+ }
+
+ private static class RavenwoodInstanceOuterRule implements TestRule {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return getCurrentRunner().wrapWithHooks(
+ base, description, Scope.Instance, Order.Outer);
+ }
+ }
+
+ private static class RavenwoodInstanceInnerRule implements TestRule {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return getCurrentRunner().wrapWithHooks(
+ base, description, Scope.Instance, Order.Inner);
+ }
+ }
+
+ public static final TestRule sImplicitClassOuterRule = new RavenwoodClassOuterRule();
+ public static final TestRule sImplicitClassInnerRule = new RavenwoodClassInnerRule();
+ public static final TestRule sImplicitInstOuterRule = new RavenwoodInstanceOuterRule();
+ public static final TestRule sImplicitInstInnerRule = new RavenwoodInstanceOuterRule();
+
+ public static final String IMPLICIT_CLASS_OUTER_RULE_NAME = "sImplicitClassOuterRule";
+ public static final String IMPLICIT_CLASS_INNER_RULE_NAME = "sImplicitClassInnerRule";
+ public static final String IMPLICIT_INST_OUTER_RULE_NAME = "sImplicitInstOuterRule";
+ public static final String IMPLICIT_INST_INNER_RULE_NAME = "sImplicitInstInnerRule";
+
+ /** Keeps track of the runner on the current thread. */
+ private static final ThreadLocal<RavenwoodAwareTestRunner> sCurrentRunner = new ThreadLocal<>();
+
+ static RavenwoodAwareTestRunner getCurrentRunner() {
+ var runner = sCurrentRunner.get();
+ if (runner == null) {
+ throw new RuntimeException("Current test runner not set!");
+ }
+ return runner;
+ }
+
+ private final Class<?> mTestJavaClass;
+ private TestClass mTestClass = null;
+ private Runner mRealRunner = null;
+ private Description mDescription = null;
+ private Throwable mExceptionInConstructor = null;
+ private boolean mRealRunnerTakesRunnerBuilder = false;
+
+ /**
+ * Stores internal states / methods associated with this runner that's only needed in
+ * junit-impl.
+ */
+ final RavenwoodRunnerState mState = new RavenwoodRunnerState(this);
+
+ private Error logAndFail(String message, Throwable exception) {
+ Log.e(TAG, message, exception);
+ throw new AssertionError(message, exception);
+ }
+
+ public TestClass getTestClass() {
+ return mTestClass;
+ }
+
+ /**
+ * Constructor.
+ */
+ public RavenwoodAwareTestRunner(Class<?> testClass) {
+ mTestJavaClass = testClass;
+ try {
+ performGlobalInitialization();
+
+ /*
+ * If the class has @DisabledOnRavenwood, then we'll delegate to
+ * ClassSkippingTestRunner, which simply skips it.
+ *
+ * We need to do it before instantiating TestClass for b/367694651.
+ */
+ if (isOnRavenwood() && !RavenwoodAwareTestRunnerHook.shouldRunClassOnRavenwood(
+ testClass)) {
+ mRealRunner = new ClassSkippingTestRunner(testClass);
+ mDescription = mRealRunner.getDescription();
+ return;
+ }
+
+ mTestClass = new TestClass(testClass);
+
+ Log.v(TAG, "RavenwoodAwareTestRunner starting for " + testClass.getCanonicalName());
+
+ onRunnerInitializing();
+
+ // Find the real runner.
+ final Class<? extends Runner> realRunnerClass;
+ final InnerRunner innerRunnerAnnotation = mTestClass.getAnnotation(InnerRunner.class);
+ if (innerRunnerAnnotation != null) {
+ realRunnerClass = innerRunnerAnnotation.value();
+ } else {
+ // Default runner.
+ realRunnerClass = BlockJUnit4ClassRunner.class;
+ }
+
+ try {
+ Log.i(TAG, "Initializing the inner runner: " + realRunnerClass);
+
+ mRealRunner = instantiateRealRunner(realRunnerClass, testClass);
+ mDescription = mRealRunner.getDescription();
+
+ } catch (InstantiationException | IllegalAccessException
+ | InvocationTargetException | NoSuchMethodException e) {
+ throw logAndFail("Failed to instantiate " + realRunnerClass, e);
+ }
+ } catch (Throwable th) {
+ // If we throw in the constructor, Tradefed may not report it and just ignore the class,
+ // so record it and throw it when the test actually started.
+ Log.e(TAG, "Fatal: Exception detected in constructor", th);
+ mExceptionInConstructor = new RuntimeException("Exception detected in constructor",
+ th);
+ mDescription = Description.createTestDescription(testClass, "Constructor");
+
+ // This is for testing if tradefed is fixed.
+ if ("1".equals(System.getenv("RAVENWOOD_THROW_EXCEPTION_IN_TEST_RUNNER"))) {
+ throw th;
+ }
+ }
+ }
+
+ private Runner instantiateRealRunner(
+ Class<? extends Runner> realRunnerClass,
+ Class<?> testClass)
+ throws NoSuchMethodException, InvocationTargetException, InstantiationException,
+ IllegalAccessException {
+ try {
+ return realRunnerClass.getConstructor(Class.class).newInstance(testClass);
+ } catch (NoSuchMethodException e) {
+ var constructor = realRunnerClass.getConstructor(Class.class, RunnerBuilder.class);
+ mRealRunnerTakesRunnerBuilder = true;
+ return constructor.newInstance(testClass, new AllDefaultPossibilitiesBuilder());
+ }
+ }
+
+ private void performGlobalInitialization() {
+ if (!isOnRavenwood()) {
+ return;
+ }
+ RavenwoodAwareTestRunnerHook.performGlobalInitialization();
+ }
+
+ /**
+ * Run the bare minimum setup to initialize the wrapped runner.
+ */
+ // This method is called by the ctor, so never make it virtual.
+ private void onRunnerInitializing() {
+ if (!isOnRavenwood()) {
+ return;
+ }
+
+ RavenwoodAwareTestRunnerHook.onRunnerInitializing(this, mTestClass);
+
+ // Hook point to allow more customization.
+ runAnnotatedMethodsOnRavenwood(RavenwoodTestRunnerInitializing.class, null);
+ }
+
+ private void runAnnotatedMethodsOnRavenwood(Class<? extends Annotation> annotationClass,
+ Object instance) {
+ if (!isOnRavenwood()) {
+ return;
+ }
+ Log.v(TAG, "runAnnotatedMethodsOnRavenwood() " + annotationClass.getName());
+
+ for (var method : getTestClass().getAnnotatedMethods(annotationClass)) {
+ ensureIsPublicVoidMethod(method.getMethod(), /* isStatic=*/ instance == null);
+
+ var methodDesc = method.getDeclaringClass().getName() + "."
+ + method.getMethod().toString();
+ try {
+ method.getMethod().invoke(instance);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw logAndFail("Caught exception while running method " + methodDesc, e);
+ }
+ }
+ }
+
+ @Override
+ public Description getDescription() {
+ return mDescription;
+ }
+
+ @Override
+ public void run(RunNotifier realNotifier) {
+ final RavenwoodRunNotifier notifier = new RavenwoodRunNotifier(realNotifier);
+
+ if (mRealRunner instanceof ClassSkippingTestRunner) {
+ mRealRunner.run(notifier);
+ RavenwoodAwareTestRunnerHook.onClassSkipped(getDescription());
+ return;
+ }
+
+ Log.v(TAG, "Starting " + mTestJavaClass.getCanonicalName());
+ if (RAVENWOOD_VERBOSE_LOGGING) {
+ dumpDescription(getDescription());
+ }
+
+ if (maybeReportExceptionFromConstructor(notifier)) {
+ return;
+ }
+
+ // TODO(b/365976974): handle nested classes better
+ final boolean skipRunnerHook =
+ mRealRunnerTakesRunnerBuilder && mRealRunner instanceof Suite;
+
+ sCurrentRunner.set(this);
+ try {
+ if (!skipRunnerHook) {
+ try {
+ RavenwoodAwareTestRunnerHook.onBeforeInnerRunnerStart(
+ this, getDescription());
+ } catch (Throwable th) {
+ notifier.reportBeforeTestFailure(getDescription(), th);
+ return;
+ }
+ }
+
+ // Delegate to the inner runner.
+ mRealRunner.run(notifier);
+ } finally {
+ sCurrentRunner.remove();
+
+ if (!skipRunnerHook) {
+ try {
+ RavenwoodAwareTestRunnerHook.onAfterInnerRunnerFinished(
+ this, getDescription());
+ } catch (Throwable th) {
+ notifier.reportAfterTestFailure(th);
+ }
+ }
+ }
+ }
+
+ /** Throw the exception detected in the constructor, if any. */
+ private boolean maybeReportExceptionFromConstructor(RunNotifier notifier) {
+ if (mExceptionInConstructor == null) {
+ return false;
+ }
+ notifier.fireTestStarted(mDescription);
+ notifier.fireTestFailure(new Failure(mDescription, mExceptionInConstructor));
+ notifier.fireTestFinished(mDescription);
+
+ return true;
+ }
+
+ @Override
+ public void filter(Filter filter) throws NoTestsRemainException {
+ if (mRealRunner instanceof Filterable r) {
+ r.filter(filter);
+ }
+ }
+
+ @Override
+ public void order(Orderer orderer) throws InvalidOrderingException {
+ if (mRealRunner instanceof Orderable r) {
+ r.order(orderer);
+ }
+ }
+
+ @Override
+ public void sort(Sorter sorter) {
+ if (mRealRunner instanceof Sortable r) {
+ r.sort(sorter);
+ }
+ }
+
+ private Statement wrapWithHooks(Statement base, Description description, Scope scope,
+ Order order) {
+ if (!isOnRavenwood()) {
+ return base;
+ }
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ runWithHooks(description, scope, order, base);
+ }
+ };
+ }
+
+ private void runWithHooks(Description description, Scope scope, Order order, Runnable r)
+ throws Throwable {
+ runWithHooks(description, scope, order, new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ r.run();
+ }
+ });
+ }
+
+ private void runWithHooks(Description description, Scope scope, Order order, Statement s)
+ throws Throwable {
+ if (isOnRavenwood()) {
+ Assume.assumeTrue(
+ RavenwoodAwareTestRunnerHook.onBefore(this, description, scope, order));
+ }
+ try {
+ s.evaluate();
+ if (isOnRavenwood()) {
+ RavenwoodAwareTestRunnerHook.onAfter(this, description, scope, order, null);
+ }
+ } catch (Throwable t) {
+ boolean shouldThrow = true;
+ if (isOnRavenwood()) {
+ shouldThrow = RavenwoodAwareTestRunnerHook.onAfter(
+ this, description, scope, order, t);
+ }
+ if (shouldThrow) {
+ throw t;
+ }
+ }
+ }
+
+ /**
+ * A runner that simply skips a class. It still has to support {@link Filterable}
+ * because otherwise the result still says "SKIPPED" even when it's not included in the
+ * filter.
+ */
+ private static class ClassSkippingTestRunner extends Runner implements Filterable {
+ private final Description mDescription;
+ private boolean mFilteredOut;
+
+ ClassSkippingTestRunner(Class<?> testClass) {
+ mDescription = Description.createTestDescription(testClass, testClass.getSimpleName());
+ mFilteredOut = false;
+ }
+
+ @Override
+ public Description getDescription() {
+ return mDescription;
+ }
+
+ @Override
+ public void run(RunNotifier notifier) {
+ if (mFilteredOut) {
+ return;
+ }
+ notifier.fireTestSuiteStarted(mDescription);
+ notifier.fireTestIgnored(mDescription);
+ notifier.fireTestSuiteFinished(mDescription);
+ }
+
+ @Override
+ public void filter(Filter filter) throws NoTestsRemainException {
+ if (filter.shouldRun(mDescription)) {
+ mFilteredOut = false;
+ } else {
+ throw new NoTestsRemainException();
+ }
+ }
+ }
+
+ private void dumpDescription(Description desc) {
+ dumpDescription(desc, "[TestDescription]=", " ");
+ }
+
+ private void dumpDescription(Description desc, String header, String indent) {
+ Log.v(TAG, indent + header + desc);
+
+ var children = desc.getChildren();
+ var childrenIndent = " " + indent;
+ for (int i = 0; i < children.size(); i++) {
+ dumpDescription(children.get(i), "#" + i + ": ", childrenIndent);
+ }
+ }
+
+ /**
+ * A run notifier that wraps another notifier and provides the following features:
+ * - Handle a failure that happened before testStarted and testEnded (typically that means
+ * it's from @BeforeClass or @AfterClass, or a @ClassRule) and deliver it as if
+ * individual tests in the class reported it. This is for b/364395552.
+ *
+ * - Logging.
+ */
+ private class RavenwoodRunNotifier extends RunNotifier {
+ private final RunNotifier mRealNotifier;
+
+ private final Stack<Description> mSuiteStack = new Stack<>();
+ private Description mCurrentSuite = null;
+ private final ArrayList<Throwable> mOutOfTestFailures = new ArrayList<>();
+
+ private boolean mBeforeTest = true;
+ private boolean mAfterTest = false;
+
+ private RavenwoodRunNotifier(RunNotifier realNotifier) {
+ mRealNotifier = realNotifier;
+ }
+
+ private boolean isInTest() {
+ return !mBeforeTest && !mAfterTest;
+ }
+
+ @Override
+ public void addListener(RunListener listener) {
+ mRealNotifier.addListener(listener);
+ }
+
+ @Override
+ public void removeListener(RunListener listener) {
+ mRealNotifier.removeListener(listener);
+ }
+
+ @Override
+ public void addFirstListener(RunListener listener) {
+ mRealNotifier.addFirstListener(listener);
+ }
+
+ @Override
+ public void fireTestRunStarted(Description description) {
+ Log.i(TAG, "testRunStarted: " + description);
+ mRealNotifier.fireTestRunStarted(description);
+ }
+
+ @Override
+ public void fireTestRunFinished(Result result) {
+ Log.i(TAG, "testRunFinished: "
+ + result.getRunCount() + ","
+ + result.getFailureCount() + ","
+ + result.getAssumptionFailureCount() + ","
+ + result.getIgnoreCount());
+ mRealNotifier.fireTestRunFinished(result);
+ }
+
+ @Override
+ public void fireTestSuiteStarted(Description description) {
+ Log.i(TAG, "testSuiteStarted: " + description);
+ mRealNotifier.fireTestSuiteStarted(description);
+
+ mBeforeTest = true;
+ mAfterTest = false;
+
+ // Keep track of the current suite, needed if the outer test is a Suite,
+ // in which case its children are test classes. (not test methods)
+ mCurrentSuite = description;
+ mSuiteStack.push(description);
+
+ mOutOfTestFailures.clear();
+ }
+
+ @Override
+ public void fireTestSuiteFinished(Description description) {
+ Log.i(TAG, "testSuiteFinished: " + description);
+ mRealNotifier.fireTestSuiteFinished(description);
+
+ maybeHandleOutOfTestFailures();
+
+ mBeforeTest = true;
+ mAfterTest = false;
+
+ // Restore the upper suite.
+ mSuiteStack.pop();
+ mCurrentSuite = mSuiteStack.size() == 0 ? null : mSuiteStack.peek();
+ }
+
+ @Override
+ public void fireTestStarted(Description description) throws StoppedByUserException {
+ Log.i(TAG, "testStarted: " + description);
+ mRealNotifier.fireTestStarted(description);
+
+ mAfterTest = false;
+ mBeforeTest = false;
+ }
+
+ @Override
+ public void fireTestFailure(Failure failure) {
+ Log.i(TAG, "testFailure: " + failure);
+
+ if (isInTest()) {
+ mRealNotifier.fireTestFailure(failure);
+ } else {
+ mOutOfTestFailures.add(failure.getException());
+ }
+ }
+
+ @Override
+ public void fireTestAssumptionFailed(Failure failure) {
+ Log.i(TAG, "testAssumptionFailed: " + failure);
+
+ if (isInTest()) {
+ mRealNotifier.fireTestAssumptionFailed(failure);
+ } else {
+ mOutOfTestFailures.add(failure.getException());
+ }
+ }
+
+ @Override
+ public void fireTestIgnored(Description description) {
+ Log.i(TAG, "testIgnored: " + description);
+ mRealNotifier.fireTestIgnored(description);
+ }
+
+ @Override
+ public void fireTestFinished(Description description) {
+ Log.i(TAG, "testFinished: " + description);
+ mRealNotifier.fireTestFinished(description);
+
+ mAfterTest = true;
+ }
+
+ @Override
+ public void pleaseStop() {
+ Log.w(TAG, "pleaseStop:");
+ mRealNotifier.pleaseStop();
+ }
+
+ /**
+ * At the end of each Suite, we handle failures happened out of test methods.
+ * (typically in @BeforeClass or @AfterClasses)
+ *
+ * This is to work around b/364395552.
+ */
+ private boolean maybeHandleOutOfTestFailures() {
+ if (mOutOfTestFailures.size() == 0) {
+ return false;
+ }
+ Throwable th;
+ if (mOutOfTestFailures.size() == 1) {
+ th = mOutOfTestFailures.get(0);
+ } else {
+ th = new MultipleFailureException(mOutOfTestFailures);
+ }
+ if (mBeforeTest) {
+ reportBeforeTestFailure(mCurrentSuite, th);
+ return true;
+ }
+ if (mAfterTest) {
+ reportAfterTestFailure(th);
+ return true;
+ }
+ return false;
+ }
+
+ public void reportBeforeTestFailure(Description suiteDesc, Throwable th) {
+ // If a failure happens befere running any tests, we'll need to pretend
+ // as if each test in the suite reported the failure, to work around b/364395552.
+ for (var child : suiteDesc.getChildren()) {
+ if (child.isSuite()) {
+ // If the chiil is still a "parent" -- a test class or a test suite
+ // -- propagate to its children.
+ mRealNotifier.fireTestSuiteStarted(child);
+ reportBeforeTestFailure(child, th);
+ mRealNotifier.fireTestSuiteFinished(child);
+ } else {
+ mRealNotifier.fireTestStarted(child);
+ Failure f = new Failure(child, th);
+ if (th instanceof AssumptionViolatedException) {
+ mRealNotifier.fireTestAssumptionFailed(f);
+ } else {
+ mRealNotifier.fireTestFailure(f);
+ }
+ mRealNotifier.fireTestFinished(child);
+ }
+ }
+ }
+
+ public void reportAfterTestFailure(Throwable th) {
+ // Unfortunately, there's no good way to report it, so kill the own process.
+ onCriticalError(
+ "Failures detected in @AfterClass, which would be swallowed by tradefed",
+ th);
+ }
+ }
+
+ private static volatile BiConsumer<String, Throwable> sCriticalErrorHanler;
+
+ private void onCriticalError(@NonNull String message, @Nullable Throwable th) {
+ Log.e(TAG, "Critical error! " + message, th);
+ var handler = sCriticalErrorHanler;
+ if (handler == null) {
+ handler = sDefaultCriticalErrorHandler;
+ }
+ handler.accept(message, th);
+ }
+
+ private static BiConsumer<String, Throwable> sDefaultCriticalErrorHandler = (message, th) -> {
+ Log.e(TAG, "Ravenwood cannot continue. Killing self process.", th);
+ System.exit(1);
+ };
+
+ /**
+ * Contains Ravenwood private APIs.
+ */
+ public static class RavenwoodPrivate {
+ private RavenwoodPrivate() {
+ }
+
+ /**
+ * Set a listener for onCriticalError(), for testing. If a listener is set, we won't call
+ * System.exit().
+ */
+ public void setCriticalErrorHandler(
+ @Nullable BiConsumer<String, Throwable> handler) {
+ sCriticalErrorHanler = handler;
+ }
+ }
+
+ private static final RavenwoodPrivate sRavenwoodPrivate = new RavenwoodPrivate();
+
+ public static RavenwoodPrivate private$ravenwood() {
+ return sRavenwoodPrivate;
+ }
+}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
index f4b7ec36..85297fe 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
@@ -16,42 +16,20 @@
package android.platform.test.ravenwood;
-import static android.platform.test.ravenwood.RavenwoodRule.ENABLE_PROBE_IGNORED;
-import static android.platform.test.ravenwood.RavenwoodRule.IS_ON_RAVENWOOD;
-import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnDevice;
-import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnRavenwood;
-import static android.platform.test.ravenwood.RavenwoodRule.shouldStillIgnoreInProbeIgnoreMode;
-
-import android.platform.test.annotations.DisabledOnRavenwood;
-import android.platform.test.annotations.EnabledOnRavenwood;
-
-import org.junit.Assert;
-import org.junit.Assume;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
/**
- * {@code @ClassRule} that respects Ravenwood-specific class annotations. This rule has no effect
- * when tests are run on non-Ravenwood test environments.
+ * No longer needed.
*
- * By default, all tests are executed on Ravenwood, but annotations such as
- * {@link DisabledOnRavenwood} and {@link EnabledOnRavenwood} can be used at both the method
- * and class level to "ignore" tests that may not be ready.
+ * @deprecated this class used to be used to handle the class level annotation, which
+ * is now done by the test runner, so this class is not needed.
*/
+@Deprecated
public class RavenwoodClassRule implements TestRule {
@Override
public Statement apply(Statement base, Description description) {
- if (!IS_ON_RAVENWOOD) {
- // This should be "Assume", not Assert, but if we use assume here, the device side
- // test runner would complain.
- // See the TODO comment in RavenwoodClassRuleRavenwoodOnlyTest.
- Assert.assertTrue(shouldEnableOnDevice(description));
- } else if (ENABLE_PROBE_IGNORED) {
- Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description));
- } else {
- Assume.assumeTrue(shouldEnableOnRavenwood(description));
- }
return base;
}
}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
new file mode 100644
index 0000000..446f819
--- /dev/null
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
@@ -0,0 +1,217 @@
+/*
+ * 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 android.platform.test.ravenwood;
+
+import static android.os.Process.FIRST_APPLICATION_UID;
+import static android.os.Process.SYSTEM_UID;
+import static android.os.UserHandle.SYSTEM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Instrumentation;
+import android.content.Context;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Represents how to configure the ravenwood environment for a test class.
+ *
+ * If a ravenwood test class has a public static field with the {@link Config} annotation,
+ * Ravenwood will extract the config from it and initializes the environment. The type of the
+ * field must be of {@link RavenwoodConfig}.
+ */
+public final class RavenwoodConfig {
+ /**
+ * Use this to mark a field as the configuration.
+ * @hide
+ */
+ @Target({ElementType.FIELD})
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface Config {
+ }
+
+ private static final int NOBODY_UID = 9999;
+
+ private static final AtomicInteger sNextPid = new AtomicInteger(100);
+
+ int mCurrentUser = SYSTEM.getIdentifier();
+
+ /**
+ * Unless the test author requests differently, run as "nobody", and give each collection of
+ * tests its own unique PID.
+ */
+ int mUid = NOBODY_UID;
+ int mPid = sNextPid.getAndIncrement();
+
+ String mTestPackageName;
+ String mTargetPackageName;
+
+ int mMinSdkLevel;
+
+ boolean mProvideMainThread = false;
+
+ final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties();
+
+ final List<Class<?>> mServicesRequired = new ArrayList<>();
+
+ volatile Context mInstContext;
+ volatile Context mTargetContext;
+ volatile Instrumentation mInstrumentation;
+
+ /**
+ * Stores internal states / methods associated with this config that's only needed in
+ * junit-impl.
+ */
+ final RavenwoodConfigState mState = new RavenwoodConfigState(this);
+ private RavenwoodConfig() {
+ }
+
+ /**
+ * Return if the current process is running on a Ravenwood test environment.
+ */
+ public static boolean isOnRavenwood() {
+ return RavenwoodRule.isOnRavenwood();
+ }
+
+ private void setDefaults() {
+ if (mTargetPackageName == null) {
+ mTargetPackageName = mTestPackageName;
+ }
+ }
+
+ public static class Builder {
+ private final RavenwoodConfig mConfig = new RavenwoodConfig();
+
+ public Builder() {
+ }
+
+ /**
+ * Configure the identity of this process to be the system UID for the duration of the
+ * test. Has no effect on non-Ravenwood environments.
+ */
+ public Builder setProcessSystem() {
+ mConfig.mUid = SYSTEM_UID;
+ return this;
+ }
+
+ /**
+ * Configure the identity of this process to be an app UID for the duration of the
+ * test. Has no effect on non-Ravenwood environments.
+ */
+ public Builder setProcessApp() {
+ mConfig.mUid = FIRST_APPLICATION_UID;
+ return this;
+ }
+
+ /**
+ * Configure the package name of the test, which corresponds to
+ * {@link Instrumentation#getContext()}.
+ */
+ public Builder setPackageName(@NonNull String packageName) {
+ mConfig.mTestPackageName = Objects.requireNonNull(packageName);
+ return this;
+ }
+
+ /**
+ * Configure the package name of the target app, which corresponds to
+ * {@link Instrumentation#getTargetContext()}. Defaults to {@link #setPackageName}.
+ */
+ public Builder setTargetPackageName(@NonNull String packageName) {
+ mConfig.mTargetPackageName = Objects.requireNonNull(packageName);
+ return this;
+ }
+
+ /**
+ * Configure the min SDK level of the test.
+ */
+ public Builder setMinSdkLevel(int sdkLevel) {
+ mConfig.mMinSdkLevel = sdkLevel;
+ return this;
+ }
+
+ /**
+ * Configure a "main" thread to be available for the duration of the test, as defined
+ * by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments.
+ */
+ public Builder setProvideMainThread(boolean provideMainThread) {
+ mConfig.mProvideMainThread = provideMainThread;
+ return this;
+ }
+
+ /**
+ * Configure the given system property as immutable for the duration of the test.
+ * Read access to the key is allowed, and write access will fail. When {@code value} is
+ * {@code null}, the value is left as undefined.
+ *
+ * All properties in the {@code debug.*} namespace are automatically mutable, with no
+ * developer action required.
+ *
+ * Has no effect on non-Ravenwood environments.
+ */
+ public Builder setSystemPropertyImmutable(@NonNull String key,
+ @Nullable Object value) {
+ mConfig.mSystemProperties.setValue(key, value);
+ mConfig.mSystemProperties.setAccessReadOnly(key);
+ return this;
+ }
+
+ /**
+ * Configure the given system property as mutable for the duration of the test.
+ * Both read and write access to the key is allowed, and its value will be reset between
+ * each test. When {@code value} is {@code null}, the value is left as undefined.
+ *
+ * All properties in the {@code debug.*} namespace are automatically mutable, with no
+ * developer action required.
+ *
+ * Has no effect on non-Ravenwood environments.
+ */
+ public Builder setSystemPropertyMutable(@NonNull String key,
+ @Nullable Object value) {
+ mConfig.mSystemProperties.setValue(key, value);
+ mConfig.mSystemProperties.setAccessReadWrite(key);
+ return this;
+ }
+
+ /**
+ * Configure the set of system services that are required for this test to operate.
+ *
+ * For example, passing {@code android.hardware.SerialManager.class} as an argument will
+ * ensure that the underlying service is created, initialized, and ready to use for the
+ * duration of the test. The {@code SerialManager} instance can be obtained via
+ * {@code RavenwoodRule.getContext()} and {@code Context.getSystemService()}, and
+ * {@code SerialManagerInternal} can be obtained via {@code LocalServices.getService()}.
+ */
+ public Builder setServicesRequired(@NonNull Class<?>... services) {
+ mConfig.mServicesRequired.clear();
+ for (Class<?> service : services) {
+ mConfig.mServicesRequired.add(service);
+ }
+ return this;
+ }
+
+ public RavenwoodConfig build() {
+ mConfig.setDefaults();
+ return mConfig;
+ }
+ }
+}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 825c91a..4196d8e 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -16,56 +16,43 @@
package android.platform.test.ravenwood;
-import static android.os.Process.FIRST_APPLICATION_UID;
-import static android.os.Process.SYSTEM_UID;
-import static android.os.UserHandle.SYSTEM;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.log;
-import static org.junit.Assert.fail;
-
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.Instrumentation;
import android.content.Context;
-import android.platform.test.annotations.DisabledOnNonRavenwood;
import android.platform.test.annotations.DisabledOnRavenwood;
-import android.platform.test.annotations.EnabledOnRavenwood;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
import com.android.ravenwood.common.RavenwoodCommonUtils;
-import org.junit.Assume;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Objects;
-import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
/**
- * {@code @Rule} that configures the Ravenwood test environment. This rule has no effect when
- * tests are run on non-Ravenwood test environments.
- *
- * This rule initializes and resets the Ravenwood environment between each test method to offer a
- * hermetic testing environment.
- *
- * By default, all tests are executed on Ravenwood, but annotations such as
- * {@link DisabledOnRavenwood} and {@link EnabledOnRavenwood} can be used at both the method
- * and class level to "ignore" tests that may not be ready. When needed, a
- * {@link RavenwoodClassRule} can be used in addition to a {@link RavenwoodRule} to ignore tests
- * before a test class is fully initialized.
+ * @deprecated Use {@link RavenwoodConfig} to configure the ravenwood environment instead.
+ * A {@link RavenwoodRule} is no longer needed for {@link DisabledOnRavenwood}. To get the
+ * {@link Context} and {@link Instrumentation}, use
+ * {@link androidx.test.platform.app.InstrumentationRegistry} instead.
*/
-public class RavenwoodRule implements TestRule {
+@Deprecated
+public final class RavenwoodRule implements TestRule {
+ private static final String TAG = "RavenwoodRule";
+
static final boolean IS_ON_RAVENWOOD = RavenwoodCommonUtils.isOnRavenwood();
/**
- * When probing is enabled, all tests will be unconditionally run on Ravenwood to detect
- * cases where a test is able to pass despite being marked as {@code IgnoreUnderRavenwood}.
+ * When this flag is enabled, all tests will be unconditionally run on Ravenwood to detect
+ * cases where a test is able to pass despite being marked as {@link DisabledOnRavenwood}.
*
* This is typically helpful for internal maintainers discovering tests that had previously
* been ignored, but now have enough Ravenwood-supported functionality to be enabled.
*/
- static final boolean ENABLE_PROBE_IGNORED = "1".equals(
+ private static final boolean RUN_DISABLED_TESTS = "1".equals(
System.getenv("RAVENWOOD_RUN_DISABLED_TESTS"));
/**
@@ -90,56 +77,34 @@
*
* Because we use a regex-find, setting "." would disable all tests.
*/
- private static final Pattern REALLY_DISABLE_PATTERN = Pattern.compile(
- Objects.requireNonNullElse(System.getenv("RAVENWOOD_REALLY_DISABLE"), ""));
+ private static final Pattern REALLY_DISABLED_PATTERN = Pattern.compile(
+ Objects.requireNonNullElse(System.getenv("RAVENWOOD_REALLY_DISABLED"), ""));
- private static final boolean ENABLE_REALLY_DISABLE_PATTERN =
- !REALLY_DISABLE_PATTERN.pattern().isEmpty();
-
- /**
- * If true, enable optional validation on running tests.
- */
- private static final boolean ENABLE_OPTIONAL_VALIDATION = "1".equals(
- System.getenv("RAVENWOOD_OPTIONAL_VALIDATION"));
+ private static final boolean HAS_REALLY_DISABLE_PATTERN =
+ !REALLY_DISABLED_PATTERN.pattern().isEmpty();
static {
- if (ENABLE_PROBE_IGNORED) {
- System.out.println("$RAVENWOOD_RUN_DISABLED_TESTS enabled: force running all tests");
- if (ENABLE_REALLY_DISABLE_PATTERN) {
- System.out.println("$RAVENWOOD_REALLY_DISABLE=" + REALLY_DISABLE_PATTERN.pattern());
+ if (RUN_DISABLED_TESTS) {
+ log(TAG, "$RAVENWOOD_RUN_DISABLED_TESTS enabled: force running all tests");
+ if (HAS_REALLY_DISABLE_PATTERN) {
+ log(TAG, "$RAVENWOOD_REALLY_DISABLED=" + REALLY_DISABLED_PATTERN.pattern());
}
}
}
- private static final int NOBODY_UID = 9999;
-
- private static final AtomicInteger sNextPid = new AtomicInteger(100);
-
- int mCurrentUser = SYSTEM.getIdentifier();
-
- /**
- * Unless the test author requests differently, run as "nobody", and give each collection of
- * tests its own unique PID.
- */
- int mUid = NOBODY_UID;
- int mPid = sNextPid.getAndIncrement();
-
- String mPackageName;
-
- boolean mProvideMainThread = false;
-
- final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties();
-
- final List<Class<?>> mServicesRequired = new ArrayList<>();
-
- volatile Context mContext;
- volatile Instrumentation mInstrumentation;
+ private final RavenwoodConfig mConfiguration;
public RavenwoodRule() {
+ mConfiguration = new RavenwoodConfig.Builder().build();
+ }
+
+ private RavenwoodRule(RavenwoodConfig config) {
+ mConfiguration = config;
}
public static class Builder {
- private RavenwoodRule mRule = new RavenwoodRule();
+ private final RavenwoodConfig.Builder mBuilder =
+ new RavenwoodConfig.Builder();
public Builder() {
}
@@ -149,7 +114,7 @@
* test. Has no effect on non-Ravenwood environments.
*/
public Builder setProcessSystem() {
- mRule.mUid = SYSTEM_UID;
+ mBuilder.setProcessSystem();
return this;
}
@@ -158,7 +123,7 @@
* test. Has no effect on non-Ravenwood environments.
*/
public Builder setProcessApp() {
- mRule.mUid = FIRST_APPLICATION_UID;
+ mBuilder.setProcessApp();
return this;
}
@@ -166,8 +131,8 @@
* Configure the identity of this process to be the given package name for the duration
* of the test. Has no effect on non-Ravenwood environments.
*/
- public Builder setPackageName(/* @NonNull */ String packageName) {
- mRule.mPackageName = Objects.requireNonNull(packageName);
+ public Builder setPackageName(@NonNull String packageName) {
+ mBuilder.setPackageName(packageName);
return this;
}
@@ -176,7 +141,7 @@
* by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments.
*/
public Builder setProvideMainThread(boolean provideMainThread) {
- mRule.mProvideMainThread = provideMainThread;
+ mBuilder.setProvideMainThread(provideMainThread);
return this;
}
@@ -190,10 +155,8 @@
*
* Has no effect on non-Ravenwood environments.
*/
- public Builder setSystemPropertyImmutable(/* @NonNull */ String key,
- /* @Nullable */ Object value) {
- mRule.mSystemProperties.setValue(key, value);
- mRule.mSystemProperties.setAccessReadOnly(key);
+ public Builder setSystemPropertyImmutable(@NonNull String key, @Nullable Object value) {
+ mBuilder.setSystemPropertyImmutable(key, value);
return this;
}
@@ -207,10 +170,8 @@
*
* Has no effect on non-Ravenwood environments.
*/
- public Builder setSystemPropertyMutable(/* @NonNull */ String key,
- /* @Nullable */ Object value) {
- mRule.mSystemProperties.setValue(key, value);
- mRule.mSystemProperties.setAccessReadWrite(key);
+ public Builder setSystemPropertyMutable(@NonNull String key, @Nullable Object value) {
+ mBuilder.setSystemPropertyMutable(key, value);
return this;
}
@@ -223,16 +184,13 @@
* {@code RavenwoodRule.getContext()} and {@code Context.getSystemService()}, and
* {@code SerialManagerInternal} can be obtained via {@code LocalServices.getService()}.
*/
- public Builder setServicesRequired(Class<?>... services) {
- mRule.mServicesRequired.clear();
- for (Class<?> service : services) {
- mRule.mServicesRequired.add(service);
- }
+ public Builder setServicesRequired(@NonNull Class<?>... services) {
+ mBuilder.setServicesRequired(services);
return this;
}
public RavenwoodRule build() {
- return mRule;
+ return new RavenwoodRule(mBuilder.build());
}
}
@@ -252,184 +210,50 @@
}
/**
- * Return a {@code Context} available for usage during the currently running test case.
- *
- * Each test should obtain needed information or references via this method;
- * references must not be stored beyond the scope of a test case.
+ * @deprecated Use
+ * {@code androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getContext()}
+ * instead.
*/
+ @Deprecated
public Context getContext() {
- return Objects.requireNonNull(mContext,
+ return Objects.requireNonNull(mConfiguration.mInstContext,
"Context is only available during @Test execution");
}
/**
- * Return a {@code Instrumentation} available for usage during the currently running test case.
- *
- * Each test should obtain needed information or references via this method;
- * references must not be stored beyond the scope of a test case.
+ * @deprecated Use
+ * {@code androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()}
+ * instead.
*/
+ @Deprecated
public Instrumentation getInstrumentation() {
- return Objects.requireNonNull(mInstrumentation,
+ return Objects.requireNonNull(mConfiguration.mInstrumentation,
"Instrumentation is only available during @Test execution");
}
- static boolean shouldEnableOnDevice(Description description) {
- if (description.isTest()) {
- if (description.getAnnotation(DisabledOnNonRavenwood.class) != null) {
- return false;
- }
- }
- final var clazz = description.getTestClass();
- if (clazz != null) {
- if (clazz.getAnnotation(DisabledOnNonRavenwood.class) != null) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Determine if the given {@link Description} should be enabled when running on the
- * Ravenwood test environment.
- *
- * A more specific method-level annotation always takes precedence over any class-level
- * annotation, and an {@link EnabledOnRavenwood} annotation always takes precedence over
- * an {@link DisabledOnRavenwood} annotation.
- */
- static boolean shouldEnableOnRavenwood(Description description) {
- // First, consult any method-level annotations
- if (description.isTest()) {
- // Stopgap for http://g/ravenwood/EPAD-N5ntxM
- if (description.getMethodName().endsWith("$noRavenwood")) {
- return false;
- }
- if (description.getAnnotation(EnabledOnRavenwood.class) != null) {
- return true;
- }
- if (description.getAnnotation(DisabledOnRavenwood.class) != null) {
- return false;
- }
- if (description.getAnnotation(IgnoreUnderRavenwood.class) != null) {
- return false;
- }
- }
-
- // Otherwise, consult any class-level annotations
- final var clazz = description.getTestClass();
- if (clazz != null) {
- if (description.getTestClass().getAnnotation(EnabledOnRavenwood.class) != null) {
- return true;
- }
- if (description.getTestClass().getAnnotation(DisabledOnRavenwood.class) != null) {
- return false;
- }
- if (description.getTestClass().getAnnotation(IgnoreUnderRavenwood.class) != null) {
- return false;
- }
- }
-
- // When no annotations have been requested, assume test should be included
- return true;
- }
-
- static boolean shouldStillIgnoreInProbeIgnoreMode(Description description) {
- if (!ENABLE_REALLY_DISABLE_PATTERN) {
- return false;
- }
-
- final var fullname = description.getTestClass().getName()
- + (description.isTest() ? "#" + description.getMethodName() : "");
-
- if (REALLY_DISABLE_PATTERN.matcher(fullname).find()) {
- System.out.println("Still ignoring " + fullname);
- return true;
- }
- return false;
- }
-
@Override
public Statement apply(Statement base, Description description) {
- // No special treatment when running outside Ravenwood; run tests as-is
- if (!IS_ON_RAVENWOOD) {
- Assume.assumeTrue(shouldEnableOnDevice(description));
+ if (!RavenwoodConfig.isOnRavenwood()) {
return base;
}
-
- if (ENABLE_PROBE_IGNORED) {
- return applyProbeIgnored(base, description);
- } else {
- return applyDefault(base, description);
- }
- }
-
- private void commonPrologue(Statement base, Description description) {
- RavenwoodRuleImpl.logTestRunner("started", description);
- RavenwoodRuleImpl.validate(base, description, ENABLE_OPTIONAL_VALIDATION);
- RavenwoodRuleImpl.init(RavenwoodRule.this);
- }
-
- /**
- * Run the given {@link Statement} with no special treatment.
- */
- private Statement applyDefault(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
- Assume.assumeTrue(shouldEnableOnRavenwood(description));
-
- commonPrologue(base, description);
+ RavenwoodAwareTestRunnerHook.onRavenwoodRuleEnter(
+ RavenwoodAwareTestRunner.getCurrentRunner(), description,
+ RavenwoodRule.this);
try {
base.evaluate();
- RavenwoodRuleImpl.logTestRunner("finished", description);
- } catch (Throwable t) {
- RavenwoodRuleImpl.logTestRunner("failed", description);
- throw t;
} finally {
- RavenwoodRuleImpl.reset(RavenwoodRule.this);
+ RavenwoodAwareTestRunnerHook.onRavenwoodRuleExit(
+ RavenwoodAwareTestRunner.getCurrentRunner(), description,
+ RavenwoodRule.this);
}
}
};
}
/**
- * Run the given {@link Statement} with probing enabled. All tests will be unconditionally
- * run on Ravenwood to detect cases where a test is able to pass despite being marked as
- * {@code IgnoreUnderRavenwood}.
- */
- private Statement applyProbeIgnored(Statement base, Description description) {
- return new Statement() {
- @Override
- public void evaluate() throws Throwable {
- Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description));
-
- commonPrologue(base, description);
- try {
- base.evaluate();
- } catch (Throwable t) {
- // If the test isn't included, eat the exception and report the
- // assumption failure that test authors expect; otherwise throw
- Assume.assumeTrue(shouldEnableOnRavenwood(description));
- throw t;
- } finally {
- RavenwoodRuleImpl.logTestRunner("finished", description);
- RavenwoodRuleImpl.reset(RavenwoodRule.this);
- }
-
- if (!shouldEnableOnRavenwood(description)) {
- fail("Test wasn't included under Ravenwood, but it actually "
- + "passed under Ravenwood; consider updating annotations");
- }
- }
- };
- }
-
- public static class _$RavenwoodPrivate {
- public static boolean isOptionalValidationEnabled() {
- return ENABLE_OPTIONAL_VALIDATION;
- }
- }
-
- /**
* Returns the "real" result from {@link System#currentTimeMillis()}.
*
* Currently, it's the same thing as calling {@link System#currentTimeMillis()},
@@ -439,4 +263,51 @@
public long realCurrentTimeMillis() {
return System.currentTimeMillis();
}
+
+ // Below are internal to ravenwood. Don't use them from normal tests...
+
+ public static class RavenwoodPrivate {
+ private RavenwoodPrivate() {
+ }
+
+ private volatile Boolean mRunDisabledTestsOverride = null;
+
+ private volatile Pattern mReallyDisabledPattern = null;
+
+ public boolean isRunningDisabledTests() {
+ if (mRunDisabledTestsOverride != null) {
+ return mRunDisabledTestsOverride;
+ }
+ return RUN_DISABLED_TESTS;
+ }
+
+ public Pattern getReallyDisabledPattern() {
+ if (mReallyDisabledPattern != null) {
+ return mReallyDisabledPattern;
+ }
+ return REALLY_DISABLED_PATTERN;
+ }
+
+ public void overrideRunDisabledTest(boolean runDisabledTests,
+ @Nullable String reallyDisabledPattern) {
+ mRunDisabledTestsOverride = runDisabledTests;
+ mReallyDisabledPattern =
+ reallyDisabledPattern == null ? null : Pattern.compile(reallyDisabledPattern);
+ }
+
+ public void resetRunDisabledTest() {
+ mRunDisabledTestsOverride = null;
+ mReallyDisabledPattern = null;
+ }
+ }
+
+ private static final RavenwoodPrivate sRavenwoodPrivate = new RavenwoodPrivate();
+
+ public static RavenwoodPrivate private$ravenwood() {
+ return sRavenwoodPrivate;
+ }
+
+ RavenwoodConfig getConfiguration() {
+ return mConfiguration;
+ }
}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
index 5f1b0c2..ef8f584 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
@@ -48,11 +48,13 @@
switch (key) {
case "gsm.version.baseband":
case "no.such.thing":
+ case "qemu.sf.lcd_density":
case "ro.bootloader":
case "ro.debuggable":
case "ro.hardware":
case "ro.hw_timeout_multiplier":
case "ro.odm.build.media_performance_class":
+ case "ro.sf.lcd_density":
case "ro.treble.enabled":
case "ro.vndk.version":
return true;
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
new file mode 100644
index 0000000..aa8c299
--- /dev/null
+++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
@@ -0,0 +1,101 @@
+/*
+ * 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 android.platform.test.ravenwood;
+
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Order;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Scope;
+
+import org.junit.runner.Description;
+import org.junit.runners.model.TestClass;
+
+/**
+ * Provide hook points created by {@link RavenwoodAwareTestRunner}. This is a version
+ * that's used on a device side test.
+ *
+ * All methods are no-op in real device tests.
+ *
+ * TODO: Use some kind of factory to provide different implementation for the device test
+ * and the ravenwood test.
+ */
+public class RavenwoodAwareTestRunnerHook {
+ private RavenwoodAwareTestRunnerHook() {
+ }
+
+ /**
+ * Called before any code starts. Internally it will only initialize the environment once.
+ */
+ public static void performGlobalInitialization() {
+ }
+
+ /**
+ * Called when a runner starts, before the inner runner gets a chance to run.
+ */
+ public static void onRunnerInitializing(RavenwoodAwareTestRunner runner, TestClass testClass) {
+ }
+
+ /**
+ * Called when a whole test class is skipped.
+ */
+ public static void onClassSkipped(Description description) {
+ }
+
+ /**
+ * Called before the inner runner starts.
+ */
+ public static void onBeforeInnerRunnerStart(
+ RavenwoodAwareTestRunner runner, Description description) throws Throwable {
+ }
+
+ /**
+ * Called after the inner runner finished.
+ */
+ public static void onAfterInnerRunnerFinished(
+ RavenwoodAwareTestRunner runner, Description description) throws Throwable {
+ }
+
+ /**
+ * Called before a test / class.
+ *
+ * Return false if it should be skipped.
+ */
+ public static boolean onBefore(RavenwoodAwareTestRunner runner, Description description,
+ Scope scope, Order order) throws Throwable {
+ return true;
+ }
+
+ public static void onRavenwoodRuleEnter(RavenwoodAwareTestRunner runner,
+ Description description, RavenwoodRule rule) throws Throwable {
+ }
+
+ public static void onRavenwoodRuleExit(RavenwoodAwareTestRunner runner,
+ Description description, RavenwoodRule rule) throws Throwable {
+ }
+
+
+ /**
+ * Called after a test / class.
+ *
+ * Return false if the exception should be ignored.
+ */
+ public static boolean onAfter(RavenwoodAwareTestRunner runner, Description description,
+ Scope scope, Order order, Throwable th) {
+ return true;
+ }
+
+ public static boolean shouldRunClassOnRavenwood(Class<?> clazz) {
+ return true;
+ }
+}
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodConfigState.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodConfigState.java
new file mode 100644
index 0000000..43a28ba
--- /dev/null
+++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodConfigState.java
@@ -0,0 +1,22 @@
+/*
+ * 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 android.platform.test.ravenwood;
+
+/** Stub class. The actual implementaetion is in junit-impl-src. */
+public class RavenwoodConfigState {
+ public RavenwoodConfigState(RavenwoodConfig config) {
+ }
+}
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
deleted file mode 100644
index 483b98a..0000000
--- a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 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 android.platform.test.ravenwood;
-
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-public class RavenwoodRuleImpl {
- public static void init(RavenwoodRule rule) {
- // No-op when running on a real device
- }
-
- public static void reset(RavenwoodRule rule) {
- // No-op when running on a real device
- }
-
- public static void logTestRunner(String label, Description description) {
- // No-op when running on a real device
- }
-
- public static void validate(Statement base, Description description,
- boolean enableOptionalValidation) {
- }
-
- public static long realCurrentTimeMillis() {
- return System.currentTimeMillis();
- }
-}
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
new file mode 100644
index 0000000..83cbc52
--- /dev/null
+++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
@@ -0,0 +1,22 @@
+/*
+ * 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 android.platform.test.ravenwood;
+
+/** Stub class. The actual implementaetion is in junit-impl-src. */
+public class RavenwoodRunnerState {
+ public RavenwoodRunnerState(RavenwoodAwareTestRunner runner) {
+ }
+}
diff --git a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java b/ravenwood/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
similarity index 97%
rename from ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
rename to ravenwood/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
index b477117..30abaa2 100644
--- a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
+++ b/ravenwood/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.ravenwood;
+package com.android.ravenwoodtest;
import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
diff --git a/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java b/ravenwood/resapk_test/test/com/android/ravenwoodtest/resapk_test/RavenwoodResApkTest.java
similarity index 96%
rename from ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java
rename to ravenwood/resapk_test/test/com/android/ravenwoodtest/resapk_test/RavenwoodResApkTest.java
index 1029ed2..e547114 100644
--- a/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java
+++ b/ravenwood/resapk_test/test/com/android/ravenwoodtest/resapk_test/RavenwoodResApkTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.ravenwood.resapk_test;
+package com.android.ravenwoodtest.resapk_test;
import static junit.framework.TestCase.assertTrue;
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
index 0238baa..02153a7 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
@@ -38,7 +38,6 @@
*/
public abstract void setFdInt(FileDescriptor fd, int fdInt);
-
/**
* Equivalent to Android's FileDescriptor.getInt$().
*/
@@ -49,6 +48,10 @@
*/
public abstract void closeFd(FileDescriptor fd) throws IOException;
+ public abstract long addressOf(Object o);
+
+ public abstract <T> T fromAddress(long address);
+
/**
* Placeholder implementation for the host side.
*
@@ -75,5 +78,15 @@
public void closeFd(FileDescriptor fd) {
throw calledOnHostside();
}
+
+ @Override
+ public long addressOf(Object o) {
+ throw calledOnHostside();
+ }
+
+ @Override
+ public <T> T fromAddress(long address) {
+ throw calledOnHostside();
+ }
}
}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
index a260147..2323c65 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
@@ -18,8 +18,16 @@
import java.io.FileDescriptor;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
+import java.util.WeakHashMap;
class OpenJdkWorkaround extends JvmWorkaround {
+
+ // @GuardedBy("sAddressMap")
+ private static final Map<Object, Long> sAddressMap = new WeakHashMap<>();
+ // @GuardedBy("sAddressMap")
+ private static long sCurrentAddress = 1;
+
@Override
public void setFdInt(FileDescriptor fd, int fdInt) {
try {
@@ -60,4 +68,28 @@
+ " perhaps JRE has changed?", e);
}
}
+
+ @Override
+ public long addressOf(Object o) {
+ synchronized (sAddressMap) {
+ Long address = sAddressMap.get(o);
+ if (address == null) {
+ address = sCurrentAddress++;
+ sAddressMap.put(o, address);
+ }
+ return address;
+ }
+ }
+
+ @Override
+ public <T> T fromAddress(long address) {
+ synchronized (sAddressMap) {
+ for (var e : sAddressMap.entrySet()) {
+ if (e.getValue() == address) {
+ return (T) e.getKey();
+ }
+ }
+ }
+ return null;
+ }
}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
index c8cc8d9..989bb6b 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
@@ -15,12 +15,20 @@
*/
package com.android.ravenwood.common;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
import com.android.ravenwood.common.divergence.RavenwoodDivergence;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
import java.util.Arrays;
public class RavenwoodCommonUtils {
@@ -31,6 +39,14 @@
private static final Object sLock = new Object();
+ /**
+ * If set to "1", we enable the verbose logging.
+ *
+ * (See also InitLogging() in http://ac/system/libbase/logging.cpp)
+ */
+ public static final boolean RAVENWOOD_VERBOSE_LOGGING = "1".equals(System.getenv(
+ "RAVENWOOD_VERBOSE"));
+
/** Name of `libravenwood_runtime` */
private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
@@ -42,10 +58,19 @@
private static final boolean IS_ON_RAVENWOOD = RavenwoodDivergence.isOnRavenwood();
- private static final String RAVEWOOD_RUNTIME_PATH = getRavenwoodRuntimePathInternal();
+ private static final String RAVENWOOD_RUNTIME_PATH = getRavenwoodRuntimePathInternal();
public static final String RAVENWOOD_SYSPROP = "ro.is_on_ravenwood";
+ public static final String RAVENWOOD_RESOURCE_APK = "ravenwood-res-apks/ravenwood-res.apk";
+ public static final String RAVENWOOD_INST_RESOURCE_APK =
+ "ravenwood-res-apks/ravenwood-inst-res.apk";
+
+ public static final String RAVENWOOD_EMPTY_RESOURCES_APK =
+ RAVENWOOD_RUNTIME_PATH + "ravenwood-data/ravenwood-empty-res.apk";
+
+ public static final String RAVENWOOD_VERSION_JAVA_SYSPROP = "android.ravenwood.version";
+
// @GuardedBy("sLock")
private static boolean sIntegrityChecked = false;
@@ -72,6 +97,18 @@
return sEnableExtraRuntimeCheck;
}
+ /** Simple logging method. */
+ public static void log(String tag, String message) {
+ // Avoid using Android's Log class, which could be broken for various reasons.
+ // (e.g. the JNI file doesn't exist for whatever reason)
+ System.out.print(tag + ": " + message + "\n");
+ }
+
+ /** Simple logging method. */
+ private void log(String tag, String format, Object... args) {
+ log(tag, String.format(format, args));
+ }
+
/**
* Load the main runtime JNI library.
*/
@@ -176,7 +213,7 @@
*/
public static String getRavenwoodRuntimePath() {
ensureOnRavenwood();
- return RAVEWOOD_RUNTIME_PATH;
+ return RAVENWOOD_RUNTIME_PATH;
}
private static String getRavenwoodRuntimePathInternal() {
@@ -231,4 +268,37 @@
var is = new FileInputStream(fd);
RavenwoodCommonUtils.closeQuietly(is);
}
+
+ public static void ensureIsPublicVoidMethod(Method method, boolean isStatic) {
+ var ok = Modifier.isPublic(method.getModifiers())
+ && (Modifier.isStatic(method.getModifiers()) == isStatic)
+ && (method.getReturnType() == void.class);
+ if (ok) {
+ return; // okay
+ }
+ throw new AssertionError(String.format(
+ "Method %s.%s() expected to be public %svoid",
+ method.getDeclaringClass().getName(), method.getName(),
+ (isStatic ? "static " : "")));
+ }
+
+ public static void ensureIsPublicMember(Member member, boolean isStatic) {
+ var ok = Modifier.isPublic(member.getModifiers())
+ && (Modifier.isStatic(member.getModifiers()) == isStatic);
+ if (ok) {
+ return; // okay
+ }
+ throw new AssertionError(String.format(
+ "%s.%s expected to be public %s",
+ member.getDeclaringClass().getName(), member.getName(),
+ (isStatic ? "static" : "")));
+ }
+
+ @NonNull
+ public static String getStackTraceString(@Nullable Throwable th) {
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter writer = new PrintWriter(stringWriter);
+ th.printStackTrace(writer);
+ return stringWriter.toString();
+ }
}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java b/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
similarity index 89%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java
rename to ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
index f38d565..e21a9cd 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.database;
-import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.os.Parcel;
import android.util.Base64;
@@ -35,8 +34,8 @@
private String mName;
private int mColumnNum;
private static class Row {
- String[] fields;
- int[] types;
+ String[] mFields;
+ int[] mTypes;
}
private final List<Row> mRows = new ArrayList<>();
@@ -69,9 +68,9 @@
public static boolean nativeAllocRow(long windowPtr) {
CursorWindow_host instance = sInstances.get(windowPtr);
Row row = new Row();
- row.fields = new String[instance.mColumnNum];
- row.types = new int[instance.mColumnNum];
- Arrays.fill(row.types, Cursor.FIELD_TYPE_NULL);
+ row.mFields = new String[instance.mColumnNum];
+ row.mTypes = new int[instance.mColumnNum];
+ Arrays.fill(row.mTypes, Cursor.FIELD_TYPE_NULL);
instance.mRows.add(row);
return true;
}
@@ -82,8 +81,8 @@
return false;
}
Row r = instance.mRows.get(row);
- r.fields[column] = value;
- r.types[column] = type;
+ r.mFields[column] = value;
+ r.mTypes[column] = type;
return true;
}
@@ -93,7 +92,7 @@
return Cursor.FIELD_TYPE_NULL;
}
- return instance.mRows.get(row).types[column];
+ return instance.mRows.get(row).mTypes[column];
}
public static boolean nativePutString(long windowPtr, String value,
@@ -107,7 +106,7 @@
return null;
}
- return instance.mRows.get(row).fields[column];
+ return instance.mRows.get(row).mFields[column];
}
public static boolean nativePutLong(long windowPtr, long value, int row, int column) {
@@ -170,8 +169,8 @@
parcel.writeInt(window.mColumnNum);
parcel.writeInt(window.mRows.size());
for (int row = 0; row < window.mRows.size(); row++) {
- parcel.writeStringArray(window.mRows.get(row).fields);
- parcel.writeIntArray(window.mRows.get(row).types);
+ parcel.writeStringArray(window.mRows.get(row).mFields);
+ parcel.writeIntArray(window.mRows.get(row).mTypes);
}
}
@@ -183,8 +182,8 @@
int rowCount = parcel.readInt();
for (int row = 0; row < rowCount; row++) {
Row r = new Row();
- r.fields = parcel.createStringArray();
- r.types = parcel.createIntArray();
+ r.mFields = parcel.createStringArray();
+ r.mTypes = parcel.createIntArray();
window.mRows.add(r);
}
return windowPtr;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java b/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
similarity index 97%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java
rename to ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
index 5e81124..1b63adc 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.os;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java b/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
similarity index 89%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java
rename to ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
index e7479d3..b09bf31 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.os;
import android.util.SparseArray;
@@ -36,9 +36,6 @@
/** Predicate tested to determine if a given key can be written. */
@GuardedBy("sLock")
private static Predicate<String> sKeyWritablePredicate;
- /** Callback to trigger when values are changed */
- @GuardedBy("sLock")
- private static Runnable sChangeCallback;
/**
* Reverse mapping that provides a way back to an original key from the
@@ -48,7 +45,7 @@
private static SparseArray<String> sKeyHandles = new SparseArray<>();
/**
- * Basically the same as {@link #native_init$ravenwood}, but it'll only run if no values are
+ * Basically the same as {@link #init$ravenwood}, but it'll only run if no values are
* set yet.
*/
public static void initializeIfNeeded(Map<String, String> values,
@@ -57,30 +54,32 @@
if (sValues != null) {
return; // Already initialized.
}
- native_init$ravenwood(values, keyReadablePredicate, keyWritablePredicate,
- () -> {});
+ init$ravenwood(values, keyReadablePredicate, keyWritablePredicate);
}
}
- public static void native_init$ravenwood(Map<String, String> values,
- Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate,
- Runnable changeCallback) {
+ public static void init$ravenwood(Map<String, String> values,
+ Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) {
synchronized (sLock) {
sValues = Objects.requireNonNull(values);
sKeyReadablePredicate = Objects.requireNonNull(keyReadablePredicate);
sKeyWritablePredicate = Objects.requireNonNull(keyWritablePredicate);
- sChangeCallback = Objects.requireNonNull(changeCallback);
sKeyHandles.clear();
+ synchronized (SystemProperties.sChangeCallbacks) {
+ SystemProperties.sChangeCallbacks.clear();
+ }
}
}
- public static void native_reset$ravenwood() {
+ public static void reset$ravenwood() {
synchronized (sLock) {
sValues = null;
sKeyReadablePredicate = null;
sKeyWritablePredicate = null;
- sChangeCallback = null;
sKeyHandles.clear();
+ synchronized (SystemProperties.sChangeCallbacks) {
+ SystemProperties.sChangeCallbacks.clear();
+ }
}
}
@@ -101,7 +100,7 @@
} else {
sValues.put(key, val);
}
- sChangeCallback.run();
+ SystemProperties.callChangeCallbacks();
}
}
@@ -183,7 +182,7 @@
// Report through callback always registered via init above
synchronized (sLock) {
Preconditions.requireNonNullViaRavenwoodRule(sValues);
- sChangeCallback.run();
+ SystemProperties.callChangeCallbacks();
}
}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java b/ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java
similarity index 83%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java
rename to ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java
index 55d4ffb..878a0ff 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java
@@ -13,12 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.util;
import com.android.internal.os.RuntimeInit;
import java.io.PrintStream;
-import java.util.Collection;
public class EventLog_host {
public static int writeEvent(int tag, int value) {
@@ -58,15 +57,6 @@
return sb.length();
}
- public static void readEvents(int[] tags, Collection<android.util.EventLog.Event> output) {
- throw new UnsupportedOperationException();
- }
-
- public static void readEventsOnWrapping(int[] tags, long timestamp,
- Collection<android.util.EventLog.Event> output) {
- throw new UnsupportedOperationException();
- }
-
/**
* Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so
* that we don't end up in a recursive loop.
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java
similarity index 95%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java
rename to ravenwood/runtime-helper-src/framework/android/util/Log_host.java
index f301b9c..d232ef2 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java
@@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.util;
-import android.util.Log;
import android.util.Log.Level;
import com.android.internal.os.RuntimeInit;
@@ -44,7 +43,7 @@
case Log.LOG_ID_SYSTEM: buffer = "system"; break;
case Log.LOG_ID_CRASH: buffer = "crash"; break;
default: buffer = "buf:" + bufID; break;
- };
+ }
final String prio;
switch (priority) {
@@ -55,7 +54,7 @@
case Log.ERROR: prio = "E"; break;
case Log.ASSERT: prio = "A"; break;
default: prio = "prio:" + priority; break;
- };
+ }
for (String s : msg.split("\\n")) {
getRealOut().println(String.format("logd: [%s] %s %s: %s", buffer, prio, tag, s));
diff --git a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java
new file mode 100644
index 0000000..c18c307
--- /dev/null
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java
@@ -0,0 +1,63 @@
+/*
+ * 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.internal.os;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+public class LongArrayContainer_host {
+ private static final HashMap<Long, long[]> sInstances = new HashMap<>();
+ private static long sNextId = 1;
+
+ public static long native_init(int arrayLength) {
+ long[] array = new long[arrayLength];
+ long instanceId = sNextId++;
+ sInstances.put(instanceId, array);
+ return instanceId;
+ }
+
+ static long[] getInstance(long instanceId) {
+ return sInstances.get(instanceId);
+ }
+
+ public static void native_setValues(long instanceId, long[] values) {
+ System.arraycopy(values, 0, getInstance(instanceId), 0, values.length);
+ }
+
+ public static void native_getValues(long instanceId, long[] values) {
+ System.arraycopy(getInstance(instanceId), 0, values, 0, values.length);
+ }
+
+ public static boolean native_combineValues(long instanceId, long[] array, int[] indexMap) {
+ long[] values = getInstance(instanceId);
+
+ boolean nonZero = false;
+ Arrays.fill(array, 0);
+
+ for (int i = 0; i < values.length; i++) {
+ int index = indexMap[i];
+ if (index < 0 || index >= array.length) {
+ throw new IndexOutOfBoundsException("Index " + index + " is out of bounds: [0, "
+ + (array.length - 1) + "]");
+ }
+ if (values[i] != 0) {
+ array[index] += values[i];
+ nonZero = true;
+ }
+ }
+ return nonZero;
+ }
+}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
similarity index 87%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java
rename to ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
index 0f65544..9ce8ea8 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package com.android.internal.os;
import android.os.BadParcelableException;
import android.os.Parcel;
@@ -28,7 +28,7 @@
public class LongArrayMultiStateCounter_host {
/**
- * A reimplementation of {@link com.android.internal.os.LongArrayMultiStateCounter}, only in
+ * A reimplementation of {@link LongArrayMultiStateCounter}, only in
* Java instead of native. The majority of the code (in C++) can be found in
* /frameworks/native/libs/battery/MultiStateCounter.h
*/
@@ -257,50 +257,6 @@
}
}
- public static class LongArrayContainer_host {
- private static final HashMap<Long, long[]> sInstances = new HashMap<>();
- private static long sNextId = 1;
-
- public static long native_init(int arrayLength) {
- long[] array = new long[arrayLength];
- long instanceId = sNextId++;
- sInstances.put(instanceId, array);
- return instanceId;
- }
-
- static long[] getInstance(long instanceId) {
- return sInstances.get(instanceId);
- }
-
- public static void native_setValues(long instanceId, long[] values) {
- System.arraycopy(values, 0, getInstance(instanceId), 0, values.length);
- }
-
- public static void native_getValues(long instanceId, long[] values) {
- System.arraycopy(getInstance(instanceId), 0, values, 0, values.length);
- }
-
- public static boolean native_combineValues(long instanceId, long[] array, int[] indexMap) {
- long[] values = getInstance(instanceId);
-
- boolean nonZero = false;
- Arrays.fill(array, 0);
-
- for (int i = 0; i < values.length; i++) {
- int index = indexMap[i];
- if (index < 0 || index >= array.length) {
- throw new IndexOutOfBoundsException("Index " + index + " is out of bounds: [0, "
- + (array.length - 1) + "]");
- }
- if (values[i] != 0) {
- array[index] += values[i];
- nonZero = true;
- }
- }
- return nonZero;
- }
- }
-
private static final HashMap<Long, LongArrayMultiStateCounterRavenwood> sInstances =
new HashMap<>();
private static long sNextId = 1;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
similarity index 98%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java
rename to ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
index 9486651..1d95aa1 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.platform.test.ravenwood.nativesubstitution;
+package com.android.internal.os;
import android.os.BadParcelableException;
import android.os.Parcel;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java
new file mode 100644
index 0000000..e12ff24
--- /dev/null
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java
@@ -0,0 +1,46 @@
+/*
+ * 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.internal.ravenwood;
+
+import com.android.ravenwood.common.JvmWorkaround;
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
+public class RavenwoodEnvironment_host {
+ private RavenwoodEnvironment_host() {
+ }
+
+ /**
+ * Called from {@link RavenwoodEnvironment#ensureRavenwoodInitialized()}.
+ */
+ public static void ensureRavenwoodInitialized() {
+ // Initialization is now done by RavenwoodAwareTestRunner.
+ // Should we remove it?
+ }
+
+ /**
+ * Called from {@link RavenwoodEnvironment#getRavenwoodRuntimePath()}.
+ */
+ public static String getRavenwoodRuntimePath(RavenwoodEnvironment env) {
+ return RavenwoodCommonUtils.getRavenwoodRuntimePath();
+ }
+
+ /**
+ * Called from {@link RavenwoodEnvironment#fromAddress(long)}.
+ */
+ public static <T> T fromAddress(RavenwoodEnvironment env, long address) {
+ return JvmWorkaround.getInstance().fromAddress(address);
+ }
+}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
deleted file mode 100644
index 5a3589d..0000000
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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.nativesubstitution;
-
-import com.android.ravenwood.common.JvmWorkaround;
-
-import java.io.FileDescriptor;
-
-public class ParcelFileDescriptor_host {
- public static void setFdInt(FileDescriptor fd, int fdInt) {
- JvmWorkaround.getInstance().setFdInt(fd, fdInt);
- }
-
- public static int getFdInt(FileDescriptor fd) {
- return JvmWorkaround.getInstance().getFdInt(fd);
- }
-}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
deleted file mode 100644
index 2df93cd..0000000
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
+++ /dev/null
@@ -1,529 +0,0 @@
-/*
- * Copyright (C) 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 com.android.platform.test.ravenwood.nativesubstitution;
-
-import android.system.ErrnoException;
-import android.system.Os;
-import android.util.Log;
-
-import java.io.FileDescriptor;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicLong;
-
-/**
- * Tentative, partial implementation of the Parcel native methods, using Java's
- * {@code byte[]}.
- * (We don't use a {@link ByteBuffer} because there's enough semantics differences between Parcel
- * and {@link ByteBuffer}, and it didn't work out.
- * e.g. Parcel seems to allow moving the data position to be beyond its size? Which
- * {@link ByteBuffer} wouldn't allow...)
- */
-public class Parcel_host {
- private static final String TAG = "Parcel";
-
- private Parcel_host() {
- }
-
- private static final AtomicLong sNextId = new AtomicLong(1);
-
- private static final Map<Long, Parcel_host> sInstances = new ConcurrentHashMap<>();
-
- private boolean mDeleted = false;
-
- private byte[] mBuffer;
- private int mSize;
- private int mPos;
-
- private boolean mSensitive;
- private boolean mAllowFds;
-
- // TODO Use the actual value from Parcel.java.
- private static final int OK = 0;
-
- private final Map<Integer, FileDescriptor> mFdMap = new ConcurrentHashMap<>();
-
- private static final int FD_PLACEHOLDER = 0xDEADBEEF;
- private static final int FD_PAYLOAD_SIZE = 8;
-
- private void validate() {
- if (mDeleted) {
- // TODO: Put more info
- throw new RuntimeException("Parcel already destroyed");
- }
- }
-
- private static Parcel_host getInstance(long id) {
- Parcel_host p = sInstances.get(id);
- if (p == null) {
- // TODO: Put more info
- throw new RuntimeException("Parcel doesn't exist with id=" + id);
- }
- p.validate();
- return p;
- }
-
- /** Native method substitution */
- public static long nativeCreate() {
- final long id = sNextId.getAndIncrement();
- final Parcel_host p = new Parcel_host();
- sInstances.put(id, p);
- p.init();
- return id;
- }
-
- private void init() {
- mBuffer = new byte[0];
- mSize = 0;
- mPos = 0;
- mSensitive = false;
- mAllowFds = true;
- mFdMap.clear();
- }
-
- private void updateSize() {
- if (mSize < mPos) {
- mSize = mPos;
- }
- }
-
- /** Native method substitution */
- public static void nativeDestroy(long nativePtr) {
- getInstance(nativePtr).mDeleted = true;
- sInstances.remove(nativePtr);
- }
-
- /** Native method substitution */
- public static void nativeFreeBuffer(long nativePtr) {
- getInstance(nativePtr).freeBuffer();
- }
-
- /** Native method substitution */
- private void freeBuffer() {
- init();
- }
-
- private int getCapacity() {
- return mBuffer.length;
- }
-
- private void ensureMoreCapacity(int size) {
- ensureCapacity(mPos + size);
- }
-
- private void ensureCapacity(int targetSize) {
- if (targetSize <= getCapacity()) {
- return;
- }
- var newSize = getCapacity() * 2;
- if (newSize < targetSize) {
- newSize = targetSize;
- }
- forceSetCapacity(newSize);
- }
-
- private void forceSetCapacity(int newSize) {
- var newBuf = new byte[newSize];
-
- // Copy
- System.arraycopy(mBuffer, 0, newBuf, 0, Math.min(newSize, getCapacity()));
-
- this.mBuffer = newBuf;
- }
-
- private void ensureDataAvailable(int requestSize) {
- if (mSize - mPos < requestSize) {
- throw new RuntimeException(String.format(
- "Pacel data underflow. size=%d, pos=%d, request=%d", mSize, mPos, requestSize));
- }
- }
-
- /** Native method substitution */
- public static void nativeMarkSensitive(long nativePtr) {
- getInstance(nativePtr).mSensitive = true;
- }
-
- /** Native method substitution */
- public static int nativeDataSize(long nativePtr) {
- return getInstance(nativePtr).mSize;
- }
-
- /** Native method substitution */
- public static int nativeDataAvail(long nativePtr) {
- var p = getInstance(nativePtr);
- return p.mSize - p.mPos;
- }
-
- /** Native method substitution */
- public static int nativeDataPosition(long nativePtr) {
- return getInstance(nativePtr).mPos;
- }
-
- /** Native method substitution */
- public static int nativeDataCapacity(long nativePtr) {
- return getInstance(nativePtr).mBuffer.length;
- }
-
- /** Native method substitution */
- public static void nativeSetDataSize(long nativePtr, int size) {
- var p = getInstance(nativePtr);
- p.ensureCapacity(size);
- getInstance(nativePtr).mSize = size;
- }
-
- /** Native method substitution */
- public static void nativeSetDataPosition(long nativePtr, int pos) {
- var p = getInstance(nativePtr);
- // TODO: Should this change the size or the capacity??
- p.mPos = pos;
- }
-
- /** Native method substitution */
- public static void nativeSetDataCapacity(long nativePtr, int size) {
- if (size < 0) {
- throw new IllegalArgumentException("size < 0: size=" + size);
- }
- var p = getInstance(nativePtr);
- if (p.getCapacity() < size) {
- p.forceSetCapacity(size);
- }
- }
-
- /** Native method substitution */
- public static boolean nativePushAllowFds(long nativePtr, boolean allowFds) {
- var p = getInstance(nativePtr);
- var prev = p.mAllowFds;
- p.mAllowFds = allowFds;
- return prev;
- }
-
- /** Native method substitution */
- public static void nativeRestoreAllowFds(long nativePtr, boolean lastValue) {
- getInstance(nativePtr).mAllowFds = lastValue;
- }
-
- /** Native method substitution */
- public static void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len) {
- nativeWriteBlob(nativePtr, b, offset, len);
- }
-
- /** Native method substitution */
- public static void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len) {
- var p = getInstance(nativePtr);
-
- if (b == null) {
- nativeWriteInt(nativePtr, -1);
- } else {
- final var alignedSize = align4(len);
-
- nativeWriteInt(nativePtr, len);
-
- p.ensureMoreCapacity(alignedSize);
-
- System.arraycopy(b, offset, p.mBuffer, p.mPos, len);
- p.mPos += alignedSize;
- p.updateSize();
- }
- }
-
- /** Native method substitution */
- public static int nativeWriteInt(long nativePtr, int value) {
- var p = getInstance(nativePtr);
- p.ensureMoreCapacity(Integer.BYTES);
-
- p.mBuffer[p.mPos++] = (byte) ((value >> 24) & 0xff);
- p.mBuffer[p.mPos++] = (byte) ((value >> 16) & 0xff);
- p.mBuffer[p.mPos++] = (byte) ((value >> 8) & 0xff);
- p.mBuffer[p.mPos++] = (byte) ((value >> 0) & 0xff);
-
- p.updateSize();
-
- return OK;
- }
-
- /** Native method substitution */
- public static int nativeWriteLong(long nativePtr, long value) {
- nativeWriteInt(nativePtr, (int) (value >>> 32));
- nativeWriteInt(nativePtr, (int) (value));
- return OK;
- }
-
- /** Native method substitution */
- public static int nativeWriteFloat(long nativePtr, float val) {
- return nativeWriteInt(nativePtr, Float.floatToIntBits(val));
- }
-
- /** Native method substitution */
- public static int nativeWriteDouble(long nativePtr, double val) {
- return nativeWriteLong(nativePtr, Double.doubleToLongBits(val));
- }
-
- private static int align4(int val) {
- return ((val + 3) / 4) * 4;
- }
-
- /** Native method substitution */
- public static void nativeWriteString8(long nativePtr, String val) {
- if (val == null) {
- nativeWriteBlob(nativePtr, null, 0, 0);
- } else {
- var bytes = val.getBytes(StandardCharsets.UTF_8);
- nativeWriteBlob(nativePtr, bytes, 0, bytes.length);
- }
- }
-
- /** Native method substitution */
- public static void nativeWriteString16(long nativePtr, String val) {
- // Just reuse String8
- nativeWriteString8(nativePtr, val);
- }
-
- /** Native method substitution */
- public static byte[] nativeCreateByteArray(long nativePtr) {
- return nativeReadBlob(nativePtr);
- }
-
- /** Native method substitution */
- public static boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen) {
- if (dest == null) {
- return false;
- }
- var data = nativeReadBlob(nativePtr);
- if (data == null) {
- System.err.println("Percel has NULL, which is unexpected."); // TODO: Is this correct?
- return false;
- }
- // TODO: Make sure the check logic is correct.
- if (data.length != destLen) {
- System.err.println("Byte array size mismatch: expected="
- + data.length + " given=" + destLen);
- return false;
- }
- System.arraycopy(data, 0, dest, 0, data.length);
- return true;
- }
-
- /** Native method substitution */
- public static byte[] nativeReadBlob(long nativePtr) {
- var p = getInstance(nativePtr);
- if (p.mSize - p.mPos < 4) {
- // Match native impl that returns "null" when not enough data
- return null;
- }
- final var size = nativeReadInt(nativePtr);
- if (size == -1) {
- return null;
- }
- try {
- p.ensureDataAvailable(align4(size));
- } catch (Exception e) {
- System.err.println(e.toString());
- return null;
- }
-
- var bytes = new byte[size];
- System.arraycopy(p.mBuffer, p.mPos, bytes, 0, size);
-
- p.mPos += align4(size);
-
- return bytes;
- }
-
- /** Native method substitution */
- public static int nativeReadInt(long nativePtr) {
- var p = getInstance(nativePtr);
-
- if (p.mSize - p.mPos < 4) {
- // Match native impl that returns "0" when not enough data
- return 0;
- }
-
- var ret = (((p.mBuffer[p.mPos++] & 0xff) << 24)
- | ((p.mBuffer[p.mPos++] & 0xff) << 16)
- | ((p.mBuffer[p.mPos++] & 0xff) << 8)
- | ((p.mBuffer[p.mPos++] & 0xff) << 0));
-
- return ret;
- }
-
- /** Native method substitution */
- public static long nativeReadLong(long nativePtr) {
- return (((long) nativeReadInt(nativePtr)) << 32)
- | (((long) nativeReadInt(nativePtr)) & 0xffff_ffffL);
- }
-
- /** Native method substitution */
- public static float nativeReadFloat(long nativePtr) {
- return Float.intBitsToFloat(nativeReadInt(nativePtr));
- }
-
- /** Native method substitution */
- public static double nativeReadDouble(long nativePtr) {
- return Double.longBitsToDouble(nativeReadLong(nativePtr));
- }
-
- /** Native method substitution */
- public static String nativeReadString8(long nativePtr) {
- final var bytes = nativeReadBlob(nativePtr);
- if (bytes == null) {
- return null;
- }
- return new String(bytes, StandardCharsets.UTF_8);
- }
- public static String nativeReadString16(long nativePtr) {
- return nativeReadString8(nativePtr);
- }
-
- /** Native method substitution */
- public static byte[] nativeMarshall(long nativePtr) {
- var p = getInstance(nativePtr);
- return Arrays.copyOf(p.mBuffer, p.mSize);
- }
-
- /** Native method substitution */
- public static void nativeUnmarshall(
- long nativePtr, byte[] data, int offset, int length) {
- var p = getInstance(nativePtr);
- p.ensureMoreCapacity(length);
- System.arraycopy(data, offset, p.mBuffer, p.mPos, length);
- p.mPos += length;
- p.updateSize();
- }
-
- /** Native method substitution */
- public static int nativeCompareData(long thisNativePtr, long otherNativePtr) {
- var a = getInstance(thisNativePtr);
- var b = getInstance(otherNativePtr);
- if ((a.mSize == b.mSize) && Arrays.equals(a.mBuffer, b.mBuffer)) {
- return 0;
- } else {
- return -1;
- }
- }
-
- /** Native method substitution */
- public static boolean nativeCompareDataInRange(
- long ptrA, int offsetA, long ptrB, int offsetB, int length) {
- var a = getInstance(ptrA);
- var b = getInstance(ptrB);
- if (offsetA < 0 || offsetA + length > a.mSize) {
- throw new IllegalArgumentException();
- }
- if (offsetB < 0 || offsetB + length > b.mSize) {
- throw new IllegalArgumentException();
- }
- return Arrays.equals(Arrays.copyOfRange(a.mBuffer, offsetA, offsetA + length),
- Arrays.copyOfRange(b.mBuffer, offsetB, offsetB + length));
- }
-
- /** Native method substitution */
- public static void nativeAppendFrom(
- long thisNativePtr, long otherNativePtr, int srcOffset, int length) {
- var dst = getInstance(thisNativePtr);
- var src = getInstance(otherNativePtr);
-
- dst.ensureMoreCapacity(length);
-
- System.arraycopy(src.mBuffer, srcOffset, dst.mBuffer, dst.mPos, length);
- dst.mPos += length; // TODO: 4 byte align?
- dst.updateSize();
-
- // TODO: Update the other's position?
- }
-
- /** Native method substitution */
- public static boolean nativeHasBinders(long nativePtr) {
- // Assume false for now, because we don't support adding binders.
- return false;
- }
-
- /** Native method substitution */
- public static boolean nativeHasBindersInRange(
- long nativePtr, int offset, int length) {
- // Assume false for now, because we don't support writing FDs yet.
- return false;
- }
-
- /** Native method substitution */
- public static void nativeWriteFileDescriptor(long nativePtr, java.io.FileDescriptor val) {
- var p = getInstance(nativePtr);
-
- if (!p.mAllowFds) {
- // Simulate the FDS_NOT_ALLOWED case in frameworks/base/core/jni/android_util_Binder.cpp
- throw new RuntimeException("Not allowed to write file descriptors here");
- }
-
- FileDescriptor dup = null;
- try {
- dup = Os.dup(val);
- } catch (ErrnoException e) {
- throw new RuntimeException(e);
- }
- p.mFdMap.put(p.mPos, dup);
-
- // Parcel.cpp writes two int32s for a FD.
- // Make sure FD_PAYLOAD_SIZE is in sync with this code.
- nativeWriteInt(nativePtr, FD_PLACEHOLDER);
- nativeWriteInt(nativePtr, FD_PLACEHOLDER);
- }
-
- /** Native method substitution */
- public static java.io.FileDescriptor nativeReadFileDescriptor(long nativePtr) {
- var p = getInstance(nativePtr);
-
- var pos = p.mPos;
- var fd = p.mFdMap.get(pos);
-
- if (fd == null) {
- Log.w(TAG, "nativeReadFileDescriptor: Not a FD at pos #" + pos);
- return null;
- }
- nativeReadInt(nativePtr);
- return fd;
- }
-
- /** Native method substitution */
- public static boolean nativeHasFileDescriptors(long nativePtr) {
- var p = getInstance(nativePtr);
- return p.mFdMap.size() > 0;
- }
-
- /** Native method substitution */
- public static boolean nativeHasFileDescriptorsInRange(long nativePtr, int offset, int length) {
- var p = getInstance(nativePtr);
-
- // Original code: hasFileDescriptorsInRange() in frameworks/native/libs/binder/Parcel.cpp
- if (offset < 0 || length < 0) {
- throw new IllegalArgumentException("Negative value not allowed: offset=" + offset
- + " length=" + length);
- }
- long limit = (long) offset + (long) length;
- if (limit > p.mSize) {
- throw new IllegalArgumentException("Out of range: offset=" + offset
- + " length=" + length + " dataSize=" + p.mSize);
- }
-
- for (var pos : p.mFdMap.keySet()) {
- if (offset <= pos && (pos + FD_PAYLOAD_SIZE - 1) < (offset + length)) {
- return true;
- }
- }
- return false;
- }
-}
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java
deleted file mode 100644
index b00cee0..0000000
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.nativesubstitution;
-
-import android.platform.test.ravenwood.RavenwoodSystemProperties;
-import android.util.Log;
-
-import com.android.internal.ravenwood.RavenwoodEnvironment;
-import com.android.ravenwood.common.RavenwoodCommonUtils;
-
-public class RavenwoodEnvironment_host {
- private static final String TAG = RavenwoodEnvironment.TAG;
-
- private static final Object sInitializeLock = new Object();
-
- // @GuardedBy("sInitializeLock")
- private static boolean sInitialized;
-
- private RavenwoodEnvironment_host() {
- }
-
- /**
- * Called from {@link RavenwoodEnvironment#ensureRavenwoodInitialized()}.
- */
- public static void ensureRavenwoodInitializedInternal() {
- synchronized (sInitializeLock) {
- if (sInitialized) {
- return;
- }
- Log.i(TAG, "Initializing Ravenwood environment");
-
- // Set the default values.
- var sysProps = RavenwoodSystemProperties.DEFAULT_VALUES;
-
- // We have a method that does it in RavenwoodRuleImpl, but we can't use that class
- // here, So just inline it.
- SystemProperties_host.initializeIfNeeded(
- sysProps.getValues(),
- sysProps.getKeyReadablePredicate(),
- sysProps.getKeyWritablePredicate());
-
- sInitialized = true;
- }
- }
-}
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
index e198646..be8c443 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
@@ -15,49 +15,10 @@
*/
package com.android.platform.test.ravenwood.runtimehelper;
-import com.android.ravenwood.common.RavenwoodCommonUtils;
-
-import java.io.File;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-
/**
* Standard class loader hook.
- *
- * Currently, we use this class to load libandroid_runtime (if needed). In the future, we may
- * load other JNI or do other set up here.
*/
public class ClassLoadHook {
- /**
- * If true, we won't load `libandroid_runtime`
- *
- * <p>Looks like there's some complexity in running a host test with JNI with `atest`,
- * so we need a way to remove the dependency.
- */
- private static final boolean SKIP_LOADING_LIBANDROID = "1".equals(System.getenv(
- "RAVENWOOD_SKIP_LOADING_LIBANDROID"));
-
- public static final String CORE_NATIVE_CLASSES = "core_native_classes";
- public static final String ICU_DATA_PATH = "icu.data.path";
- public static final String KEYBOARD_PATHS = "keyboard_paths";
- public static final String GRAPHICS_NATIVE_CLASSES = "graphics_native_classes";
-
- public static final String LIBANDROID_RUNTIME_NAME = "android_runtime";
-
- /**
- * Extra strings needed to pass to register_android_graphics_classes().
- *
- * `android.graphics.Graphics` is not actually a class, so we can't use the same initialization
- * strategy than the "normal" classes. So we just hardcode it here.
- */
- public static final String GRAPHICS_EXTRA_INIT_PARAMS = ",android.graphics.Graphics";
-
- private static String sInitialDir = new File("").getAbsolutePath();
-
- static {
- log("Initialized. Current dir=" + sInitialDir);
- }
-
private ClassLoadHook() {
}
@@ -70,129 +31,12 @@
public static void onClassLoaded(Class<?> clazz) {
System.out.println("Framework class loaded: " + clazz.getCanonicalName());
- loadFrameworkNativeCode();
- }
-
- private static void log(String message) {
- System.out.println("ClassLoadHook: " + message);
- }
-
- private static void log(String fmt, Object... args) {
- log(String.format(fmt, args));
- }
-
- private static void ensurePropertyNotSet(String key) {
- if (System.getProperty(key) != null) {
- throw new RuntimeException("System property \"" + key + "\" is set unexpectedly");
+ // Always try to initialize the environment in case classes are loaded before
+ // RavenwoodAwareTestRunner is initialized
+ try {
+ Class.forName("android.platform.test.ravenwood.RavenwoodRuntimeEnvironmentController")
+ .getMethod("globalInitOnce").invoke(null);
+ } catch (ReflectiveOperationException ignored) {
}
}
-
- private static void setProperty(String key, String value) {
- System.setProperty(key, value);
- log("Property set: %s=\"%s\"", key, value);
- }
-
- private static void dumpSystemProperties() {
- for (var prop : System.getProperties().entrySet()) {
- log(" %s=\"%s\"", prop.getKey(), prop.getValue());
- }
- }
-
- private static boolean sLoadFrameworkNativeCodeCalled = false;
-
- /**
- * Load `libandroid_runtime` if needed.
- */
- private static void loadFrameworkNativeCode() {
- // This is called from class-initializers, so no synchronization is needed.
- if (sLoadFrameworkNativeCodeCalled) {
- return;
- }
- sLoadFrameworkNativeCodeCalled = true;
-
- // libandroid_runtime uses Java's system properties to decide what JNI methods to set up.
- // Set up these properties for host-side tests.
-
- if ("1".equals(System.getenv("RAVENWOOD_DUMP_PROPERTIES"))) {
- log("Java system properties:");
- dumpSystemProperties();
- }
-
- if (SKIP_LOADING_LIBANDROID) {
- log("Skip loading native runtime.");
- return;
- }
-
- // Make sure these properties are not set.
- ensurePropertyNotSet(CORE_NATIVE_CLASSES);
- ensurePropertyNotSet(ICU_DATA_PATH);
- ensurePropertyNotSet(KEYBOARD_PATHS);
- ensurePropertyNotSet(GRAPHICS_NATIVE_CLASSES);
-
- // Load the libraries, if needed.
- final var libanrdoidClasses = getClassesWithNativeMethods(sLibandroidClasses);
- final var libhwuiClasses = getClassesWithNativeMethods(sLibhwuiClasses);
- if (libanrdoidClasses.isEmpty() && libhwuiClasses.isEmpty()) {
- log("No classes require JNI methods, skip loading native runtime.");
- return;
- }
- setProperty(CORE_NATIVE_CLASSES, libanrdoidClasses);
- setProperty(GRAPHICS_NATIVE_CLASSES, libhwuiClasses + GRAPHICS_EXTRA_INIT_PARAMS);
-
- log("Loading " + LIBANDROID_RUNTIME_NAME + " for '" + libanrdoidClasses + "' and '"
- + libhwuiClasses + "'");
- RavenwoodCommonUtils.loadJniLibrary(LIBANDROID_RUNTIME_NAME);
- }
-
- /**
- * Classes with native methods that are backed by libandroid_runtime.
- *
- * See frameworks/base/core/jni/platform/host/HostRuntime.cpp
- */
- private static final Class<?>[] sLibandroidClasses = {
- android.util.Log.class,
- };
-
- /**
- * Classes with native methods that are backed by libhwui.
- *
- * See frameworks/base/libs/hwui/apex/LayoutlibLoader.cpp
- */
- private static final Class<?>[] sLibhwuiClasses = {
- android.graphics.Interpolator.class,
- android.graphics.Matrix.class,
- android.graphics.Path.class,
- android.graphics.Color.class,
- android.graphics.ColorSpace.class,
- };
-
- /**
- * @return if a given class and its nested classes, if any, have any native method or not.
- */
- private static boolean hasNativeMethod(Class<?> clazz) {
- for (var nestedClass : clazz.getNestMembers()) {
- for (var method : nestedClass.getDeclaredMethods()) {
- if (Modifier.isNative(method.getModifiers())) {
- return true;
- }
- }
- }
- return false;
- }
- /**
- * Create a list of classes as comma-separated that require JNI methods to be set up from
- * a given class list, ignoring classes with no native methods.
- */
- private static String getClassesWithNativeMethods(Class<?>[] classes) {
- final var coreNativeClassesToLoad = new ArrayList<String>();
-
- for (var clazz : classes) {
- if (hasNativeMethod(clazz)) {
- log("Class %s has native methods", clazz.getCanonicalName());
- coreNativeClassesToLoad.add(clazz.getName());
- }
- }
-
- return String.join(",", coreNativeClassesToLoad);
- }
}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
index ecaa816..c94ef31 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
@@ -15,11 +15,15 @@
*/
package android.system;
+import com.android.ravenwood.RavenwoodRuntimeNative;
import com.android.ravenwood.common.JvmWorkaround;
-import com.android.ravenwood.common.RavenwoodRuntimeNative;
import java.io.FileDescriptor;
+import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousCloseException;
/**
* OS class replacement used on Ravenwood. For now, we just implement APIs as we need them...
@@ -36,6 +40,11 @@
return RavenwoodRuntimeNative.pipe2(flags);
}
+ /** Ravenwood version of the OS API. */
+ public static FileDescriptor[] pipe() throws ErrnoException {
+ return RavenwoodRuntimeNative.pipe2(0);
+ }
+
public static FileDescriptor dup(FileDescriptor fd) throws ErrnoException {
return RavenwoodRuntimeNative.dup(fd);
}
@@ -69,4 +78,23 @@
public static FileDescriptor open(String path, int flags, int mode) throws ErrnoException {
return RavenwoodRuntimeNative.open(path, flags, mode);
}
+
+ /** Ravenwood version of the OS API. */
+ public static int pread(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount,
+ long offset) throws ErrnoException, InterruptedIOException {
+ var channel = new FileInputStream(fd).getChannel();
+ var buf = ByteBuffer.wrap(bytes, byteOffset, byteCount);
+ try {
+ return channel.read(buf, offset);
+ } catch (AsynchronousCloseException e) {
+ throw new InterruptedIOException(e.getMessage());
+ } catch (IOException e) {
+ // Most likely EIO
+ throw new ErrnoException("pread", OsConstants.EIO, e);
+ }
+ }
+
+ public static void setenv(String name, String value, boolean overwrite) throws ErrnoException {
+ RavenwoodRuntimeNative.setenv(name, value, overwrite);
+ }
}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodJdkPatch.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodJdkPatch.java
new file mode 100644
index 0000000..96aed4b
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodJdkPatch.java
@@ -0,0 +1,49 @@
+/*
+ * 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.ravenwood;
+
+import com.android.ravenwood.common.JvmWorkaround;
+
+import java.io.FileDescriptor;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Class to host APIs that exist in libcore, but not in standard JRE.
+ */
+public class RavenwoodJdkPatch {
+ /**
+ * Implements FileDescriptor.getInt$()
+ */
+ public static int getInt$(FileDescriptor fd) {
+ return JvmWorkaround.getInstance().getFdInt(fd);
+ }
+
+ /**
+ * Implements FileDescriptor.setInt$(int)
+ */
+ public static void setInt$(FileDescriptor fd, int rawFd) {
+ JvmWorkaround.getInstance().setFdInt(fd, rawFd);
+ }
+
+ /**
+ * Implements LinkedHashMap.eldest()
+ */
+ public static <K, V> Map.Entry<K, V> eldest(LinkedHashMap<K, V> map) {
+ final var it = map.entrySet().iterator();
+ return it.hasNext() ? it.next() : null;
+ }
+}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java
similarity index 92%
rename from ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
rename to ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java
index beba8339..ad80d92 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java
@@ -13,11 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.ravenwood.common;
+package com.android.ravenwood;
import android.system.ErrnoException;
import android.system.StructStat;
+import com.android.ravenwood.common.JvmWorkaround;
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
import java.io.FileDescriptor;
/**
@@ -50,6 +53,9 @@
private static native int nOpen(String path, int flags, int mode) throws ErrnoException;
+ public static native void setenv(String name, String value, boolean overwrite)
+ throws ErrnoException;
+
public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence);
}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
index 7d2b00d..ba89f71 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
@@ -19,6 +19,8 @@
// The original is here:
// $ANDROID_BUILD_TOP/libcore/libart/src/main/java/dalvik/system/VMRuntime.java
+import com.android.ravenwood.common.JvmWorkaround;
+
import java.lang.reflect.Array;
public class VMRuntime {
@@ -32,14 +34,22 @@
}
public boolean is64Bit() {
- return true;
+ return "amd64".equals(System.getProperty("os.arch"));
}
public static boolean is64BitAbi(String abi) {
- return true;
+ return abi.contains("64");
}
public Object newUnpaddedArray(Class<?> componentType, int minLength) {
return Array.newInstance(componentType, minLength);
}
+
+ public Object newNonMovableArray(Class<?> componentType, int length) {
+ return Array.newInstance(componentType, length);
+ }
+
+ public long addressOf(Object obj) {
+ return JvmWorkaround.getInstance().addressOf(obj);
+ }
}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java
index 65c285e..2bd1ae8 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java
@@ -16,7 +16,13 @@
package libcore.io;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.ravenwood.common.JvmWorkaround;
+
import java.io.File;
+import java.io.FileDescriptor;
import java.io.IOException;
import java.net.Socket;
@@ -47,6 +53,13 @@
}
}
+ public static void closeQuietly(FileDescriptor fd) {
+ try {
+ Os.close(fd);
+ } catch (ErrnoException ignored) {
+ }
+ }
+
public static void deleteContents(File dir) throws IOException {
File[] files = dir.listFiles();
if (files != null) {
@@ -58,4 +71,17 @@
}
}
}
+
+ /**
+ * FD owners currently unsupported under Ravenwood; ignored
+ */
+ public static void setFdOwner(FileDescriptor fd, Object owner) {
+ }
+
+ /**
+ * FD owners currently unsupported under Ravenwood; return FD directly
+ */
+ public static int acquireRawFd(FileDescriptor fd) {
+ return JvmWorkaround.getInstance().getFdInt(fd);
+ }
}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/FP16.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/FP16.java
new file mode 100644
index 0000000..478503b
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/FP16.java
@@ -0,0 +1,814 @@
+/*
+ * Copyright (C) 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 libcore.util;
+
+/**
+ * <p>The {@code FP16} class is a wrapper and a utility class to manipulate half-precision 16-bit
+ * <a href="https://en.wikipedia.org/wiki/Half-precision_floating-point_format">IEEE 754</a>
+ * floating point data types (also called fp16 or binary16). A half-precision float can be
+ * created from or converted to single-precision floats, and is stored in a short data type.
+ *
+ * <p>The IEEE 754 standard specifies an fp16 as having the following format:</p>
+ * <ul>
+ * <li>Sign bit: 1 bit</li>
+ * <li>Exponent width: 5 bits</li>
+ * <li>Significand: 10 bits</li>
+ * </ul>
+ *
+ * <p>The format is laid out as follows:</p>
+ * <pre>
+ * 1 11111 1111111111
+ * ^ --^-- -----^----
+ * sign | |_______ significand
+ * |
+ * -- exponent
+ * </pre>
+ *
+ * <p>Half-precision floating points can be useful to save memory and/or
+ * bandwidth at the expense of range and precision when compared to single-precision
+ * floating points (fp32).</p>
+ * <p>To help you decide whether fp16 is the right storage type for you need, please
+ * refer to the table below that shows the available precision throughout the range of
+ * possible values. The <em>precision</em> column indicates the step size between two
+ * consecutive numbers in a specific part of the range.</p>
+ *
+ * <table summary="Precision of fp16 across the range">
+ * <tr><th>Range start</th><th>Precision</th></tr>
+ * <tr><td>0</td><td>1 ⁄ 16,777,216</td></tr>
+ * <tr><td>1 ⁄ 16,384</td><td>1 ⁄ 16,777,216</td></tr>
+ * <tr><td>1 ⁄ 8,192</td><td>1 ⁄ 8,388,608</td></tr>
+ * <tr><td>1 ⁄ 4,096</td><td>1 ⁄ 4,194,304</td></tr>
+ * <tr><td>1 ⁄ 2,048</td><td>1 ⁄ 2,097,152</td></tr>
+ * <tr><td>1 ⁄ 1,024</td><td>1 ⁄ 1,048,576</td></tr>
+ * <tr><td>1 ⁄ 512</td><td>1 ⁄ 524,288</td></tr>
+ * <tr><td>1 ⁄ 256</td><td>1 ⁄ 262,144</td></tr>
+ * <tr><td>1 ⁄ 128</td><td>1 ⁄ 131,072</td></tr>
+ * <tr><td>1 ⁄ 64</td><td>1 ⁄ 65,536</td></tr>
+ * <tr><td>1 ⁄ 32</td><td>1 ⁄ 32,768</td></tr>
+ * <tr><td>1 ⁄ 16</td><td>1 ⁄ 16,384</td></tr>
+ * <tr><td>1 ⁄ 8</td><td>1 ⁄ 8,192</td></tr>
+ * <tr><td>1 ⁄ 4</td><td>1 ⁄ 4,096</td></tr>
+ * <tr><td>1 ⁄ 2</td><td>1 ⁄ 2,048</td></tr>
+ * <tr><td>1</td><td>1 ⁄ 1,024</td></tr>
+ * <tr><td>2</td><td>1 ⁄ 512</td></tr>
+ * <tr><td>4</td><td>1 ⁄ 256</td></tr>
+ * <tr><td>8</td><td>1 ⁄ 128</td></tr>
+ * <tr><td>16</td><td>1 ⁄ 64</td></tr>
+ * <tr><td>32</td><td>1 ⁄ 32</td></tr>
+ * <tr><td>64</td><td>1 ⁄ 16</td></tr>
+ * <tr><td>128</td><td>1 ⁄ 8</td></tr>
+ * <tr><td>256</td><td>1 ⁄ 4</td></tr>
+ * <tr><td>512</td><td>1 ⁄ 2</td></tr>
+ * <tr><td>1,024</td><td>1</td></tr>
+ * <tr><td>2,048</td><td>2</td></tr>
+ * <tr><td>4,096</td><td>4</td></tr>
+ * <tr><td>8,192</td><td>8</td></tr>
+ * <tr><td>16,384</td><td>16</td></tr>
+ * <tr><td>32,768</td><td>32</td></tr>
+ * </table>
+ *
+ * <p>This table shows that numbers higher than 1024 lose all fractional precision.</p>
+ *
+ * @hide
+ */
+
+public final class FP16 {
+ /**
+ * The number of bits used to represent a half-precision float value.
+ *
+ * @hide
+ */
+ public static final int SIZE = 16;
+
+ /**
+ * Epsilon is the difference between 1.0 and the next value representable
+ * by a half-precision floating-point.
+ *
+ * @hide
+ */
+ public static final short EPSILON = (short) 0x1400;
+
+ /**
+ * Maximum exponent a finite half-precision float may have.
+ *
+ * @hide
+ */
+ public static final int MAX_EXPONENT = 15;
+ /**
+ * Minimum exponent a normalized half-precision float may have.
+ *
+ * @hide
+ */
+ public static final int MIN_EXPONENT = -14;
+
+ /**
+ * Smallest negative value a half-precision float may have.
+ *
+ * @hide
+ */
+ public static final short LOWEST_VALUE = (short) 0xfbff;
+ /**
+ * Maximum positive finite value a half-precision float may have.
+ *
+ * @hide
+ */
+ public static final short MAX_VALUE = (short) 0x7bff;
+ /**
+ * Smallest positive normal value a half-precision float may have.
+ *
+ * @hide
+ */
+ public static final short MIN_NORMAL = (short) 0x0400;
+ /**
+ * Smallest positive non-zero value a half-precision float may have.
+ *
+ * @hide
+ */
+ public static final short MIN_VALUE = (short) 0x0001;
+ /**
+ * A Not-a-Number representation of a half-precision float.
+ *
+ * @hide
+ */
+ public static final short NaN = (short) 0x7e00;
+ /**
+ * Negative infinity of type half-precision float.
+ *
+ * @hide
+ */
+ public static final short NEGATIVE_INFINITY = (short) 0xfc00;
+ /**
+ * Negative 0 of type half-precision float.
+ *
+ * @hide
+ */
+ public static final short NEGATIVE_ZERO = (short) 0x8000;
+ /**
+ * Positive infinity of type half-precision float.
+ *
+ * @hide
+ */
+ public static final short POSITIVE_INFINITY = (short) 0x7c00;
+ /**
+ * Positive 0 of type half-precision float.
+ *
+ * @hide
+ */
+ public static final short POSITIVE_ZERO = (short) 0x0000;
+
+ /**
+ * The offset to shift by to obtain the sign bit.
+ *
+ * @hide
+ */
+ public static final int SIGN_SHIFT = 15;
+
+ /**
+ * The offset to shift by to obtain the exponent bits.
+ *
+ * @hide
+ */
+ public static final int EXPONENT_SHIFT = 10;
+
+ /**
+ * The bitmask to AND a number with to obtain the sign bit.
+ *
+ * @hide
+ */
+ public static final int SIGN_MASK = 0x8000;
+
+ /**
+ * The bitmask to AND a number shifted by {@link #EXPONENT_SHIFT} right, to obtain exponent bits.
+ *
+ * @hide
+ */
+ public static final int SHIFTED_EXPONENT_MASK = 0x1f;
+
+ /**
+ * The bitmask to AND a number with to obtain significand bits.
+ *
+ * @hide
+ */
+ public static final int SIGNIFICAND_MASK = 0x3ff;
+
+ /**
+ * The bitmask to AND with to obtain exponent and significand bits.
+ *
+ * @hide
+ */
+ public static final int EXPONENT_SIGNIFICAND_MASK = 0x7fff;
+
+ /**
+ * The offset of the exponent from the actual value.
+ *
+ * @hide
+ */
+ public static final int EXPONENT_BIAS = 15;
+
+ private static final int FP32_SIGN_SHIFT = 31;
+ private static final int FP32_EXPONENT_SHIFT = 23;
+ private static final int FP32_SHIFTED_EXPONENT_MASK = 0xff;
+ private static final int FP32_SIGNIFICAND_MASK = 0x7fffff;
+ private static final int FP32_EXPONENT_BIAS = 127;
+ private static final int FP32_QNAN_MASK = 0x400000;
+ private static final int FP32_DENORMAL_MAGIC = 126 << 23;
+ private static final float FP32_DENORMAL_FLOAT = Float.intBitsToFloat(FP32_DENORMAL_MAGIC);
+
+ /** Hidden constructor to prevent instantiation. */
+ private FP16() {}
+
+ /**
+ * <p>Compares the two specified half-precision float values. The following
+ * conditions apply during the comparison:</p>
+ *
+ * <ul>
+ * <li>{@link #NaN} is considered by this method to be equal to itself and greater
+ * than all other half-precision float values (including {@code #POSITIVE_INFINITY})</li>
+ * <li>{@link #POSITIVE_ZERO} is considered by this method to be greater than
+ * {@link #NEGATIVE_ZERO}.</li>
+ * </ul>
+ *
+ * @param x The first half-precision float value to compare.
+ * @param y The second half-precision float value to compare
+ *
+ * @return The value {@code 0} if {@code x} is numerically equal to {@code y}, a
+ * value less than {@code 0} if {@code x} is numerically less than {@code y},
+ * and a value greater than {@code 0} if {@code x} is numerically greater
+ * than {@code y}
+ *
+ * @hide
+ */
+ public static int compare(short x, short y) {
+ if (less(x, y)) return -1;
+ if (greater(x, y)) return 1;
+
+ // Collapse NaNs, akin to halfToIntBits(), but we want to keep
+ // (signed) short value types to preserve the ordering of -0.0
+ // and +0.0
+ short xBits = isNaN(x) ? NaN : x;
+ short yBits = isNaN(y) ? NaN : y;
+
+ return (xBits == yBits ? 0 : (xBits < yBits ? -1 : 1));
+ }
+
+ /**
+ * Returns the closest integral half-precision float value to the specified
+ * half-precision float value. Special values are handled in the
+ * following ways:
+ * <ul>
+ * <li>If the specified half-precision float is NaN, the result is NaN</li>
+ * <li>If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)</li>
+ * <li>If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)</li>
+ * </ul>
+ *
+ * @param h A half-precision float value
+ * @return The value of the specified half-precision float rounded to the nearest
+ * half-precision float value
+ *
+ * @hide
+ */
+ public static short rint(short h) {
+ int bits = h & 0xffff;
+ int abs = bits & EXPONENT_SIGNIFICAND_MASK;
+ int result = bits;
+
+ if (abs < 0x3c00) {
+ result &= SIGN_MASK;
+ if (abs > 0x3800){
+ result |= 0x3c00;
+ }
+ } else if (abs < 0x6400) {
+ int exp = 25 - (abs >> 10);
+ int mask = (1 << exp) - 1;
+ result += ((1 << (exp - 1)) - (~(abs >> exp) & 1));
+ result &= ~mask;
+ }
+ if (isNaN((short) result)) {
+ // if result is NaN mask with qNaN
+ // (i.e. mask the most significant mantissa bit with 1)
+ // to comply with hardware implementations (ARM64, Intel, etc).
+ result |= NaN;
+ }
+
+ return (short) result;
+ }
+
+ /**
+ * Returns the smallest half-precision float value toward negative infinity
+ * greater than or equal to the specified half-precision float value.
+ * Special values are handled in the following ways:
+ * <ul>
+ * <li>If the specified half-precision float is NaN, the result is NaN</li>
+ * <li>If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)</li>
+ * <li>If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)</li>
+ * </ul>
+ *
+ * @param h A half-precision float value
+ * @return The smallest half-precision float value toward negative infinity
+ * greater than or equal to the specified half-precision float value
+ *
+ * @hide
+ */
+ public static short ceil(short h) {
+ int bits = h & 0xffff;
+ int abs = bits & EXPONENT_SIGNIFICAND_MASK;
+ int result = bits;
+
+ if (abs < 0x3c00) {
+ result &= SIGN_MASK;
+ result |= 0x3c00 & -(~(bits >> 15) & (abs != 0 ? 1 : 0));
+ } else if (abs < 0x6400) {
+ abs = 25 - (abs >> 10);
+ int mask = (1 << abs) - 1;
+ result += mask & ((bits >> 15) - 1);
+ result &= ~mask;
+ }
+ if (isNaN((short) result)) {
+ // if result is NaN mask with qNaN
+ // (i.e. mask the most significant mantissa bit with 1)
+ // to comply with hardware implementations (ARM64, Intel, etc).
+ result |= NaN;
+ }
+
+ return (short) result;
+ }
+
+ /**
+ * Returns the largest half-precision float value toward positive infinity
+ * less than or equal to the specified half-precision float value.
+ * Special values are handled in the following ways:
+ * <ul>
+ * <li>If the specified half-precision float is NaN, the result is NaN</li>
+ * <li>If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)</li>
+ * <li>If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)</li>
+ * </ul>
+ *
+ * @param h A half-precision float value
+ * @return The largest half-precision float value toward positive infinity
+ * less than or equal to the specified half-precision float value
+ *
+ * @hide
+ */
+ public static short floor(short h) {
+ int bits = h & 0xffff;
+ int abs = bits & EXPONENT_SIGNIFICAND_MASK;
+ int result = bits;
+
+ if (abs < 0x3c00) {
+ result &= SIGN_MASK;
+ result |= 0x3c00 & (bits > 0x8000 ? 0xffff : 0x0);
+ } else if (abs < 0x6400) {
+ abs = 25 - (abs >> 10);
+ int mask = (1 << abs) - 1;
+ result += mask & -(bits >> 15);
+ result &= ~mask;
+ }
+ if (isNaN((short) result)) {
+ // if result is NaN mask with qNaN
+ // i.e. (Mask the most significant mantissa bit with 1)
+ result |= NaN;
+ }
+
+ return (short) result;
+ }
+
+ /**
+ * Returns the truncated half-precision float value of the specified
+ * half-precision float value. Special values are handled in the following ways:
+ * <ul>
+ * <li>If the specified half-precision float is NaN, the result is NaN</li>
+ * <li>If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)</li>
+ * <li>If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)</li>
+ * </ul>
+ *
+ * @param h A half-precision float value
+ * @return The truncated half-precision float value of the specified
+ * half-precision float value
+ *
+ * @hide
+ */
+ public static short trunc(short h) {
+ int bits = h & 0xffff;
+ int abs = bits & EXPONENT_SIGNIFICAND_MASK;
+ int result = bits;
+
+ if (abs < 0x3c00) {
+ result &= SIGN_MASK;
+ } else if (abs < 0x6400) {
+ abs = 25 - (abs >> 10);
+ int mask = (1 << abs) - 1;
+ result &= ~mask;
+ }
+
+ return (short) result;
+ }
+
+ /**
+ * Returns the smaller of two half-precision float values (the value closest
+ * to negative infinity). Special values are handled in the following ways:
+ * <ul>
+ * <li>If either value is NaN, the result is NaN</li>
+ * <li>{@link #NEGATIVE_ZERO} is smaller than {@link #POSITIVE_ZERO}</li>
+ * </ul>
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ * @return The smaller of the two specified half-precision values
+ *
+ * @hide
+ */
+ public static short min(short x, short y) {
+ if (isNaN(x)) return NaN;
+ if (isNaN(y)) return NaN;
+
+ if ((x & EXPONENT_SIGNIFICAND_MASK) == 0 && (y & EXPONENT_SIGNIFICAND_MASK) == 0) {
+ return (x & SIGN_MASK) != 0 ? x : y;
+ }
+
+ return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) <
+ ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff) ? x : y;
+ }
+
+ /**
+ * Returns the larger of two half-precision float values (the value closest
+ * to positive infinity). Special values are handled in the following ways:
+ * <ul>
+ * <li>If either value is NaN, the result is NaN</li>
+ * <li>{@link #POSITIVE_ZERO} is greater than {@link #NEGATIVE_ZERO}</li>
+ * </ul>
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return The larger of the two specified half-precision values
+ *
+ * @hide
+ */
+ public static short max(short x, short y) {
+ if (isNaN(x)) return NaN;
+ if (isNaN(y)) return NaN;
+
+ if ((x & EXPONENT_SIGNIFICAND_MASK) == 0 && (y & EXPONENT_SIGNIFICAND_MASK) == 0) {
+ return (x & SIGN_MASK) != 0 ? y : x;
+ }
+
+ return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) >
+ ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff) ? x : y;
+ }
+
+ /**
+ * Returns true if the first half-precision float value is less (smaller
+ * toward negative infinity) than the second half-precision float value.
+ * If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is less than y, false otherwise
+ *
+ * @hide
+ */
+ public static boolean less(short x, short y) {
+ if (isNaN(x)) return false;
+ if (isNaN(y)) return false;
+
+ return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) <
+ ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
+ }
+
+ /**
+ * Returns true if the first half-precision float value is less (smaller
+ * toward negative infinity) than or equal to the second half-precision
+ * float value. If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is less than or equal to y, false otherwise
+ *
+ * @hide
+ */
+ public static boolean lessEquals(short x, short y) {
+ if (isNaN(x)) return false;
+ if (isNaN(y)) return false;
+
+ return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) <=
+ ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
+ }
+
+ /**
+ * Returns true if the first half-precision float value is greater (larger
+ * toward positive infinity) than the second half-precision float value.
+ * If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is greater than y, false otherwise
+ *
+ * @hide
+ */
+ public static boolean greater(short x, short y) {
+ if (isNaN(x)) return false;
+ if (isNaN(y)) return false;
+
+ return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) >
+ ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
+ }
+
+ /**
+ * Returns true if the first half-precision float value is greater (larger
+ * toward positive infinity) than or equal to the second half-precision float
+ * value. If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is greater than y, false otherwise
+ *
+ * @hide
+ */
+ public static boolean greaterEquals(short x, short y) {
+ if (isNaN(x)) return false;
+ if (isNaN(y)) return false;
+
+ return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) >=
+ ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
+ }
+
+ /**
+ * Returns true if the two half-precision float values are equal.
+ * If either of the values is NaN, the result is false. {@link #POSITIVE_ZERO}
+ * and {@link #NEGATIVE_ZERO} are considered equal.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is equal to y, false otherwise
+ *
+ * @hide
+ */
+ public static boolean equals(short x, short y) {
+ if (isNaN(x)) return false;
+ if (isNaN(y)) return false;
+
+ return x == y || ((x | y) & EXPONENT_SIGNIFICAND_MASK) == 0;
+ }
+
+ /**
+ * Returns true if the specified half-precision float value represents
+ * infinity, false otherwise.
+ *
+ * @param h A half-precision float value
+ * @return True if the value is positive infinity or negative infinity,
+ * false otherwise
+ *
+ * @hide
+ */
+ public static boolean isInfinite(short h) {
+ return (h & EXPONENT_SIGNIFICAND_MASK) == POSITIVE_INFINITY;
+ }
+
+ /**
+ * Returns true if the specified half-precision float value represents
+ * a Not-a-Number, false otherwise.
+ *
+ * @param h A half-precision float value
+ * @return True if the value is a NaN, false otherwise
+ *
+ * @hide
+ */
+ public static boolean isNaN(short h) {
+ return (h & EXPONENT_SIGNIFICAND_MASK) > POSITIVE_INFINITY;
+ }
+
+ /**
+ * Returns true if the specified half-precision float value is normalized
+ * (does not have a subnormal representation). If the specified value is
+ * {@link #POSITIVE_INFINITY}, {@link #NEGATIVE_INFINITY},
+ * {@link #POSITIVE_ZERO}, {@link #NEGATIVE_ZERO}, NaN or any subnormal
+ * number, this method returns false.
+ *
+ * @param h A half-precision float value
+ * @return True if the value is normalized, false otherwise
+ *
+ * @hide
+ */
+ public static boolean isNormalized(short h) {
+ return (h & POSITIVE_INFINITY) != 0 && (h & POSITIVE_INFINITY) != POSITIVE_INFINITY;
+ }
+
+ /**
+ * <p>Converts the specified half-precision float value into a
+ * single-precision float value. The following special cases are handled:</p>
+ * <ul>
+ * <li>If the input is {@link #NaN}, the returned value is {@link Float#NaN}</li>
+ * <li>If the input is {@link #POSITIVE_INFINITY} or
+ * {@link #NEGATIVE_INFINITY}, the returned value is respectively
+ * {@link Float#POSITIVE_INFINITY} or {@link Float#NEGATIVE_INFINITY}</li>
+ * <li>If the input is 0 (positive or negative), the returned value is +/-0.0f</li>
+ * <li>Otherwise, the returned value is a normalized single-precision float value</li>
+ * </ul>
+ *
+ * @param h The half-precision float value to convert to single-precision
+ * @return A normalized single-precision float value
+ *
+ * @hide
+ */
+ public static float toFloat(short h) {
+ int bits = h & 0xffff;
+ int s = bits & SIGN_MASK;
+ int e = (bits >>> EXPONENT_SHIFT) & SHIFTED_EXPONENT_MASK;
+ int m = (bits ) & SIGNIFICAND_MASK;
+
+ int outE = 0;
+ int outM = 0;
+
+ if (e == 0) { // Denormal or 0
+ if (m != 0) {
+ // Convert denorm fp16 into normalized fp32
+ float o = Float.intBitsToFloat(FP32_DENORMAL_MAGIC + m);
+ o -= FP32_DENORMAL_FLOAT;
+ return s == 0 ? o : -o;
+ }
+ } else {
+ outM = m << 13;
+ if (e == 0x1f) { // Infinite or NaN
+ outE = 0xff;
+ if (outM != 0) { // SNaNs are quieted
+ outM |= FP32_QNAN_MASK;
+ }
+ } else {
+ outE = e - EXPONENT_BIAS + FP32_EXPONENT_BIAS;
+ }
+ }
+
+ int out = (s << 16) | (outE << FP32_EXPONENT_SHIFT) | outM;
+ return Float.intBitsToFloat(out);
+ }
+
+ /**
+ * <p>Converts the specified single-precision float value into a
+ * half-precision float value. The following special cases are handled:</p>
+ * <ul>
+ * <li>If the input is NaN (see {@link Float#isNaN(float)}), the returned
+ * value is {@link #NaN}</li>
+ * <li>If the input is {@link Float#POSITIVE_INFINITY} or
+ * {@link Float#NEGATIVE_INFINITY}, the returned value is respectively
+ * {@link #POSITIVE_INFINITY} or {@link #NEGATIVE_INFINITY}</li>
+ * <li>If the input is 0 (positive or negative), the returned value is
+ * {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}</li>
+ * <li>If the input is a less than {@link #MIN_VALUE}, the returned value
+ * is flushed to {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}</li>
+ * <li>If the input is a less than {@link #MIN_NORMAL}, the returned value
+ * is a denorm half-precision float</li>
+ * <li>Otherwise, the returned value is rounded to the nearest
+ * representable half-precision float value</li>
+ * </ul>
+ *
+ * @param f The single-precision float value to convert to half-precision
+ * @return A half-precision float value
+ *
+ * @hide
+ */
+ public static short toHalf(float f) {
+ int bits = Float.floatToRawIntBits(f);
+ int s = (bits >>> FP32_SIGN_SHIFT );
+ int e = (bits >>> FP32_EXPONENT_SHIFT) & FP32_SHIFTED_EXPONENT_MASK;
+ int m = (bits ) & FP32_SIGNIFICAND_MASK;
+
+ int outE = 0;
+ int outM = 0;
+
+ if (e == 0xff) { // Infinite or NaN
+ outE = 0x1f;
+ outM = m != 0 ? 0x200 : 0;
+ } else {
+ e = e - FP32_EXPONENT_BIAS + EXPONENT_BIAS;
+ if (e >= 0x1f) { // Overflow
+ outE = 0x1f;
+ } else if (e <= 0) { // Underflow
+ if (e < -10) {
+ // The absolute fp32 value is less than MIN_VALUE, flush to +/-0
+ } else {
+ // The fp32 value is a normalized float less than MIN_NORMAL,
+ // we convert to a denorm fp16
+ m = m | 0x800000;
+ int shift = 14 - e;
+ outM = m >> shift;
+
+ int lowm = m & ((1 << shift) - 1);
+ int hway = 1 << (shift - 1);
+ // if above halfway or exactly halfway and outM is odd
+ if (lowm + (outM & 1) > hway){
+ // Round to nearest even
+ // Can overflow into exponent bit, which surprisingly is OK.
+ // This increment relies on the +outM in the return statement below
+ outM++;
+ }
+ }
+ } else {
+ outE = e;
+ outM = m >> 13;
+ // if above halfway or exactly halfway and outM is odd
+ if ((m & 0x1fff) + (outM & 0x1) > 0x1000) {
+ // Round to nearest even
+ // Can overflow into exponent bit, which surprisingly is OK.
+ // This increment relies on the +outM in the return statement below
+ outM++;
+ }
+ }
+ }
+ // The outM is added here as the +1 increments for outM above can
+ // cause an overflow in the exponent bit which is OK.
+ return (short) ((s << SIGN_SHIFT) | (outE << EXPONENT_SHIFT) + outM);
+ }
+
+ /**
+ * <p>Returns a hexadecimal string representation of the specified half-precision
+ * float value. If the value is a NaN, the result is <code>"NaN"</code>,
+ * otherwise the result follows this format:</p>
+ * <ul>
+ * <li>If the sign is positive, no sign character appears in the result</li>
+ * <li>If the sign is negative, the first character is <code>'-'</code></li>
+ * <li>If the value is inifinity, the string is <code>"Infinity"</code></li>
+ * <li>If the value is 0, the string is <code>"0x0.0p0"</code></li>
+ * <li>If the value has a normalized representation, the exponent and
+ * significand are represented in the string in two fields. The significand
+ * starts with <code>"0x1."</code> followed by its lowercase hexadecimal
+ * representation. Trailing zeroes are removed unless all digits are 0, then
+ * a single zero is used. The significand representation is followed by the
+ * exponent, represented by <code>"p"</code>, itself followed by a decimal
+ * string of the unbiased exponent</li>
+ * <li>If the value has a subnormal representation, the significand starts
+ * with <code>"0x0."</code> followed by its lowercase hexadecimal
+ * representation. Trailing zeroes are removed unless all digits are 0, then
+ * a single zero is used. The significand representation is followed by the
+ * exponent, represented by <code>"p-14"</code></li>
+ * </ul>
+ *
+ * @param h A half-precision float value
+ * @return A hexadecimal string representation of the specified value
+ *
+ * @hide
+ */
+ public static String toHexString(short h) {
+ StringBuilder o = new StringBuilder();
+
+ int bits = h & 0xffff;
+ int s = (bits >>> SIGN_SHIFT );
+ int e = (bits >>> EXPONENT_SHIFT) & SHIFTED_EXPONENT_MASK;
+ int m = (bits ) & SIGNIFICAND_MASK;
+
+ if (e == 0x1f) { // Infinite or NaN
+ if (m == 0) {
+ if (s != 0) o.append('-');
+ o.append("Infinity");
+ } else {
+ o.append("NaN");
+ }
+ } else {
+ if (s == 1) o.append('-');
+ if (e == 0) {
+ if (m == 0) {
+ o.append("0x0.0p0");
+ } else {
+ o.append("0x0.");
+ String significand = Integer.toHexString(m);
+ o.append(significand.replaceFirst("0{2,}$", ""));
+ o.append("p-14");
+ }
+ } else {
+ o.append("0x1.");
+ String significand = Integer.toHexString(m);
+ o.append(significand.replaceFirst("0{2,}$", ""));
+ o.append('p');
+ o.append(Integer.toString(e - EXPONENT_BIAS));
+ }
+ }
+
+ return o.toString();
+ }
+}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
index 14b5a4f..4e7dc5d 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
@@ -15,7 +15,7 @@
*/
package libcore.util;
-import com.android.ravenwood.common.RavenwoodRuntimeNative;
+import com.android.ravenwood.RavenwoodRuntimeNative;
import java.lang.ref.Cleaner;
import java.lang.ref.Reference;
diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp
index c804928..c255be5 100644
--- a/ravenwood/runtime-jni/ravenwood_runtime.cpp
+++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp
@@ -214,6 +214,19 @@
return throwIfMinusOne(env, "open", TEMP_FAILURE_RETRY(open(path.c_str(), flags, mode)));
}
+static void Linux_setenv(JNIEnv* env, jobject, jstring javaName, jstring javaValue,
+ jboolean overwrite) {
+ ScopedRealUtf8Chars name(env, javaName);
+ if (name.c_str() == NULL) {
+ jniThrowNullPointerException(env);
+ }
+ ScopedRealUtf8Chars value(env, javaValue);
+ if (value.c_str() == NULL) {
+ jniThrowNullPointerException(env);
+ }
+ throwIfMinusOne(env, "setenv", setenv(name.c_str(), value.c_str(), overwrite ? 1 : 0));
+}
+
// ---- Registration ----
static const JNINativeMethod sMethods[] =
@@ -227,6 +240,7 @@
{ "lstat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_lstat },
{ "stat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_stat },
{ "nOpen", "(Ljava/lang/String;II)I", (void*)Linux_open },
+ { "setenv", "(Ljava/lang/String;Ljava/lang/String;Z)V", (void*)Linux_setenv },
};
extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
@@ -245,7 +259,7 @@
g_StructStat = findClass(env, "android/system/StructStat");
g_StructTimespecClass = findClass(env, "android/system/StructTimespec");
- jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/common/RavenwoodRuntimeNative",
+ jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/RavenwoodRuntimeNative",
sMethods, NELEM(sMethods));
if (res < 0) {
return res;
diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsConstantsTest.java
similarity index 99%
rename from ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java
rename to ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsConstantsTest.java
index 3332e24..633ed4e 100644
--- a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java
+++ b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsConstantsTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.ravenwood.runtimetest;
+package com.android.ravenwoodtest.runtimetest;
// Copied from libcore/luni/src/test/java/libcore/android/system/OsConstantsTest.java
diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsTest.java
similarity index 99%
rename from ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
rename to ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsTest.java
index 05275b2..c2230c7 100644
--- a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
+++ b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.ravenwood.runtimetest;
+package com.android.ravenwoodtest.runtimetest;
import static android.system.OsConstants.S_ISBLK;
import static android.system.OsConstants.S_ISCHR;
diff --git a/ravenwood/scripts/list-ravenwood-tests.sh b/ravenwood/scripts/list-ravenwood-tests.sh
index fb9b823..05f3fdf 100755
--- a/ravenwood/scripts/list-ravenwood-tests.sh
+++ b/ravenwood/scripts/list-ravenwood-tests.sh
@@ -15,4 +15,4 @@
# List all the ravenwood test modules.
-jq -r 'to_entries[] | select( .value.compatibility_suites | index("ravenwood-tests") ) | .key' "$OUT/module-info.json"
+jq -r 'to_entries[] | select( .value.compatibility_suites | index("ravenwood-tests") ) | .key' "$OUT/module-info.json" | sort
diff --git a/ravenwood/tools/ravenizer-fake/ravenizer b/ravenwood/scripts/remove-ravenizer-output.sh
similarity index 60%
rename from ravenwood/tools/ravenizer-fake/ravenizer
rename to ravenwood/scripts/remove-ravenizer-output.sh
index 84b3c8e..be15b71 100755
--- a/ravenwood/tools/ravenizer-fake/ravenizer
+++ b/ravenwood/scripts/remove-ravenizer-output.sh
@@ -13,19 +13,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# "Fake" ravenizer, which just copies the file.
-# We need it to add ravenizer support to Soong on AOSP,
-# when the actual ravenizer is not in AOSP yet.
+# Delete all the ravenizer output jar files from Soong's intermediate directory.
-invalid_arg() {
- echo "Ravenizer(fake): invalid args" 1>&2
- exit 1
-}
+# `-a -prune` is needed because otherwise find would be confused if the directory disappears.
-(( $# >= 4 )) || invalid_arg
-[[ "$1" == "--in-jar" ]] || invalid_arg
-[[ "$3" == "--out-jar" ]] || invalid_arg
-
-echo "Ravenizer(fake): copiyng $2 to $4"
-
-cp "$2" "$4"
+find "${ANDROID_BUILD_TOP:?}/out/soong/.intermediates/" \
+ -type d \
+ -name 'ravenizer' \
+ -print \
+ -exec rm -fr \{\} \; \
+ -a -prune
diff --git a/ravenwood/scripts/shrink-systemui-test b/ravenwood/scripts/shrink-systemui-test
new file mode 100755
index 0000000..8589c1d
--- /dev/null
+++ b/ravenwood/scripts/shrink-systemui-test
@@ -0,0 +1,131 @@
+#!/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.
+
+set -e
+
+SCRIPT_NAME="${0##*/}"
+
+usage() {
+ cat <<"EOF"
+
+$SCRIPT_NAME: Shrink / unshrink SystemUiRavenTests.
+
+ SystemUiRavenTests has a lot of kotlin source files, so it's slow to build,
+ which is painful when you want to run it after updating ravenwood code
+ that SystemUiRavenTests depends on. (example: junit-src/)
+
+ This script basically removes the test files in SystemUI/multivalentTests
+ that don't have @EnabledOnRavenwood. But if we actaully remove them,
+ soong would re-generate the ninja file, which will take a long time,
+ so instead it'll truncate them.
+
+ This script will also tell git to ignore these files, so they won't shw up
+ in `git status`.
+ (Use `git ls-files -v | sed -ne "s/^[a-zS] //p"` to show ignored filse.)
+
+Usage:
+ $SCRIPT_NAME -s # Shrink the test files.
+
+ $SCRIPT_NAME -u # Undo it.
+
+EOF
+}
+
+TEST_PATH=${ANDROID_BUILD_TOP}/frameworks/base/packages/SystemUI/multivalentTests
+cd "$TEST_PATH"
+
+command=""
+case "$1" in
+ "-s") command=shrink ;;
+ "-u") command=unshrink ;;
+ *) usage ; exit 1 ;;
+esac
+
+
+echo "Listing test files...."
+files=( $(find . -name '*Test.kt' -o -name '*Test.java') )
+
+exemption='(BaseHeadsUpManagerTest)'
+
+shrink() {
+ local target=()
+ for file in ${files[@]}; do
+ # Check for exemption
+ if echo $file | egrep -q "$exemption"; then
+ echo " Skip exempted file"
+ continue
+ fi
+
+ echo "Checking $file"
+ if ! [[ -f $file ]] ; then
+ echo " Skip non regular file"
+ continue
+ fi
+
+ if ! [[ -s $file ]] ; then
+ echo " Skip empty file"
+ continue
+ fi
+
+ if grep -q '@EnabledOnRavenwood' $file ; then
+ echo " Skip ravenwood test file".
+ continue
+ fi
+
+ # It's a non ravenwood test file. Empty it.
+ : > $file
+
+ # Tell git to ignore the file
+
+ target+=($file)
+
+ echo " Emptied"
+
+ done
+ if (( ${#target[@]} == 0 )) ; then
+ echo "No files emptied."
+ return 0
+ fi
+
+ git update-index --skip-worktree ${target[@]}
+
+ echo "Emptied ${#target[@]} files"
+ return 0
+}
+
+unshrink() {
+ local target=()
+
+ # Collect empty files
+ for file in ${files[@]}; do
+ if [[ -s $file ]] ; then
+ continue
+ fi
+
+ target+=($file)
+ : > $file
+ done
+ if (( ${#target[@]} == 0 )) ; then
+ echo "No files to restore."
+ return 0
+ fi
+ # Un-ignore the files, and check out the original files
+ echo "Restoring ${#target[@]} files..."
+ git update-index --no-skip-worktree ${target[@]}
+ git checkout goog/main ${target[@]}
+ return 0
+}
+
+$command
diff --git a/ravenwood/scripts/update-test-mapping.sh b/ravenwood/scripts/update-test-mapping.sh
new file mode 100755
index 0000000..e478b50
--- /dev/null
+++ b/ravenwood/scripts/update-test-mapping.sh
@@ -0,0 +1,85 @@
+#!/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.
+
+# Update f/b/r/TEST_MAPPING with all the ravenwood tests as presubmit.
+#
+# Note, before running it, make sure module-info.json is up-to-date by running
+# (any) build.
+
+set -e
+
+# Tests that shouldn't be in presubmit.
+EXEMPT='^(SystemUiRavenTests)$'
+
+main() {
+ local script_name="${0##*/}"
+ local script_dir="${0%/*}"
+ local test_mapping="$script_dir/../TEST_MAPPING"
+ local test_mapping_bak="$script_dir/../TEST_MAPPING.bak"
+
+ local header="$(sed -ne '1,/AUTO-GENERATED-START/p' "$test_mapping")"
+ local footer="$(sed -ne '/AUTO-GENERATED-END/,$p' "$test_mapping")"
+
+ echo "Getting all tests"
+ local tests=( $("$script_dir/list-ravenwood-tests.sh" | grep -vP "$EXEMPT") )
+
+ local num_tests="${#tests[@]}"
+
+ if (( $num_tests == 0 )) ; then
+ echo "Something went wrong. No ravenwood tests detected." 1>&2
+ return 1
+ fi
+
+ echo "Tests: ${tests[@]}"
+
+ echo "Creating backup at $test_mapping_bak"
+ cp "$test_mapping" "$test_mapping_bak"
+
+ echo "Updating $test_mapping"
+ {
+ echo "$header"
+
+ echo " // DO NOT MODIFY MANUALLY"
+ echo " // Use scripts/$script_name to update it."
+
+ local i=0
+ while (( $i < $num_tests )) ; do
+ local comma=","
+ if (( $i == ($num_tests - 1) )); then
+ comma=""
+ fi
+ echo " {"
+ echo " \"name\": \"${tests[$i]}\","
+ echo " \"host\": true"
+ echo " }$comma"
+
+ i=$(( $i + 1 ))
+ done
+
+ echo "$footer"
+ } >"$test_mapping"
+
+ if cmp "$test_mapping_bak" "$test_mapping" ; then
+ echo "No change detecetd."
+ return 0
+ fi
+ echo "Updated $test_mapping"
+
+ # `|| true` is needed because of `set -e`.
+ diff -u "$test_mapping_bak" "$test_mapping" || true
+ return 0
+}
+
+main
diff --git a/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java b/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java
index 044239f..8ce15f0 100644
--- a/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java
+++ b/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java
@@ -16,12 +16,14 @@
package com.android.ravenwoodtest.servicestest;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import android.content.Context;
import android.hardware.SerialManager;
import android.hardware.SerialManagerInternal;
+import android.platform.test.annotations.DisabledOnRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -56,13 +58,16 @@
}
@Test
+ @DisabledOnRavenwood(reason="AOSP is missing resources support")
public void testSimple() {
// Verify that we can obtain a manager, and talk to the backend service, and that no
// serial ports are configured by default
final SerialManager service = (SerialManager)
mRavenwood.getContext().getSystemService(Context.SERIAL_SERVICE);
final String[] ports = service.getSerialPorts();
- assertEquals(0, ports.length);
+ final String[] refPorts = mRavenwood.getContext().getResources().getStringArray(
+ com.android.internal.R.array.config_serialPorts);
+ assertArrayEquals(refPorts, ports);
}
@Test
diff --git a/ravenwood/test-authors.md b/ravenwood/test-authors.md
index 0a0b200..c29fb7f 100644
--- a/ravenwood/test-authors.md
+++ b/ravenwood/test-authors.md
@@ -46,7 +46,7 @@
* Write your unit test just like you would for an Android device:
```
-import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.annotations.DisabledOnRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -66,7 +66,7 @@
* APIs available under Ravenwood are stateless by default. If your test requires explicit states (such as defining the UID you’re running under, or requiring a main `Looper` thread), add a `RavenwoodRule` to declare that:
```
-import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.annotations.DisabledOnRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.runner.AndroidJUnit4;
@@ -165,7 +165,7 @@
}
@Test
- @IgnoreUnderRavenwood(blockedBy = PackageManager.class)
+ @DisabledOnRavenwood(blockedBy = PackageManager.class)
public void testComplex() {
// Complex test that runs on devices, but is ignored under Ravenwood
}
diff --git a/ravenwood/tests/bivalentinst/Android.bp b/ravenwood/tests/bivalentinst/Android.bp
new file mode 100644
index 0000000..41e45e5
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/Android.bp
@@ -0,0 +1,148 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_ravenwood_test {
+ name: "RavenwoodBivalentInstTest_self_inst",
+
+ srcs: [
+ "test/**/*.java",
+ ],
+ exclude_srcs: [
+ "test/**/*_nonself.java",
+ ],
+
+ static_libs: [
+ "RavenwoodBivalentInstTest_self_inst_device_R",
+
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+
+ "junit",
+ "truth",
+ ],
+ resource_apk: "RavenwoodBivalentInstTest_self_inst_device",
+ auto_gen_config: true,
+}
+
+android_ravenwood_test {
+ name: "RavenwoodBivalentInstTest_nonself_inst",
+
+ srcs: [
+ "test/**/*.java",
+ ],
+ exclude_srcs: [
+ "test/**/*_self.java",
+ ],
+
+ static_libs: [
+ "RavenwoodBivalentInstTestTarget_R",
+ "RavenwoodBivalentInstTest_nonself_inst_device_R",
+
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+
+ "junit",
+ "truth",
+ ],
+ resource_apk: "RavenwoodBivalentInstTestTarget",
+ inst_resource_apk: "RavenwoodBivalentInstTest_nonself_inst_device",
+ auto_gen_config: true,
+}
+
+// We have 3 R.javas from the 3 packages (2 test apks below, and 1 target APK)
+// RavenwoodBivalentInstTest needs to use all of them, but we can't add all the
+// {.aapt.srcjar}'s together because that'd cause
+// "duplicate declaration of androidx.test.core.R$string."
+// So we build them as separate libraries, and include them as static_libs.
+java_library {
+ name: "RavenwoodBivalentInstTestTarget_R",
+ srcs: [
+ ":RavenwoodBivalentInstTestTarget{.aapt.srcjar}",
+ ],
+}
+
+java_library {
+ name: "RavenwoodBivalentInstTest_self_inst_device_R",
+ srcs: [
+ ":RavenwoodBivalentInstTest_self_inst_device{.aapt.srcjar}",
+ ],
+}
+
+java_library {
+ name: "RavenwoodBivalentInstTest_nonself_inst_device_R",
+ srcs: [
+ ":RavenwoodBivalentInstTest_nonself_inst_device{.aapt.srcjar}",
+ ],
+}
+
+android_test {
+ name: "RavenwoodBivalentInstTest_self_inst_device",
+
+ srcs: [
+ "test/**/*.java",
+ ],
+ exclude_srcs: [
+ "test/**/*_nonself.java",
+ ],
+ static_libs: [
+ "junit",
+ "truth",
+
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+
+ "ravenwood-junit",
+ ],
+ test_suites: [
+ "device-tests",
+ ],
+ use_resource_processor: false,
+ manifest: "AndroidManifest-self-inst.xml",
+ test_config: "AndroidTest-self-inst.xml",
+ optimize: {
+ enabled: false,
+ },
+}
+
+android_test {
+ name: "RavenwoodBivalentInstTest_nonself_inst_device",
+
+ srcs: [
+ "test/**/*.java",
+ ],
+ exclude_srcs: [
+ "test/**/*_self.java",
+ ],
+ static_libs: [
+ "junit",
+ "truth",
+
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+
+ "ravenwood-junit",
+ ],
+ data: [
+ ":RavenwoodBivalentInstTestTarget",
+ ],
+ test_suites: [
+ "device-tests",
+ ],
+ use_resource_processor: false,
+ manifest: "AndroidManifest-nonself-inst.xml",
+ test_config: "AndroidTest-nonself-inst.xml",
+ instrumentation_for: "RavenwoodBivalentInstTestTarget",
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/ravenwood/tests/bivalentinst/AndroidManifest-nonself-inst.xml b/ravenwood/tests/bivalentinst/AndroidManifest-nonself-inst.xml
new file mode 100644
index 0000000..a5a1f17
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/AndroidManifest-nonself-inst.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.ravenwood.bivalentinsttest_nonself_inst">
+
+ <application android:debuggable="true" >
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.ravenwood.bivalentinst_target_app"
+ />
+</manifest>
diff --git a/ravenwood/tests/bivalentinst/AndroidManifest-self-inst.xml b/ravenwood/tests/bivalentinst/AndroidManifest-self-inst.xml
new file mode 100644
index 0000000..3dc4c56
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/AndroidManifest-self-inst.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.ravenwood.bivalentinsttest_self_inst">
+
+ <application android:debuggable="true" >
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.ravenwood.bivalentinsttest_self_inst"
+ />
+</manifest>
diff --git a/ravenwood/tests/bivalentinst/AndroidTest-nonself-inst.xml b/ravenwood/tests/bivalentinst/AndroidTest-nonself-inst.xml
new file mode 100644
index 0000000..9491c53
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/AndroidTest-nonself-inst.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration>
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="RavenwoodBivalentInstTestTarget.apk" />
+ <option name="test-file-name" value="RavenwoodBivalentInstTest_nonself_inst_device.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.ravenwood.bivalentinsttest_nonself_inst" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/ravenwood/tests/bivalentinst/AndroidTest-self-inst.xml b/ravenwood/tests/bivalentinst/AndroidTest-self-inst.xml
new file mode 100644
index 0000000..3079c06
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/AndroidTest-self-inst.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration>
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="RavenwoodBivalentInstTest_self_inst_device.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.ravenwood.bivalentinsttest_self_inst" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/ravenwood/tests/bivalentinst/res/values/strings.xml b/ravenwood/tests/bivalentinst/res/values/strings.xml
new file mode 100644
index 0000000..73ef650
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string translatable="false" name="test_string_in_test">String in test APK</string>
+</resources>
diff --git a/ravenwood/tools/ravenizer-fake/Android.bp b/ravenwood/tests/bivalentinst/targetapp/Android.bp
similarity index 61%
rename from ravenwood/tools/ravenizer-fake/Android.bp
rename to ravenwood/tests/bivalentinst/targetapp/Android.bp
index 7e2c407..7528a62 100644
--- a/ravenwood/tools/ravenizer-fake/Android.bp
+++ b/ravenwood/tests/bivalentinst/targetapp/Android.bp
@@ -7,8 +7,14 @@
default_applicable_licenses: ["frameworks_base_license"],
}
-sh_binary_host {
- name: "ravenizer",
- src: "ravenizer",
- visibility: ["//visibility:public"],
+android_app {
+ name: "RavenwoodBivalentInstTestTarget",
+ srcs: [
+ "src/**/*.java",
+ ],
+ sdk_version: "current",
+ optimize: {
+ enabled: false,
+ },
+ use_resource_processor: false,
}
diff --git a/ravenwood/tests/bivalentinst/targetapp/AndroidManifest.xml b/ravenwood/tests/bivalentinst/targetapp/AndroidManifest.xml
new file mode 100644
index 0000000..0715f5d
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/targetapp/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.ravenwood.bivalentinst_target_app">
+ <application>
+ </application>
+</manifest>
diff --git a/ravenwood/tests/bivalentinst/targetapp/res/values/strings.xml b/ravenwood/tests/bivalentinst/targetapp/res/values/strings.xml
new file mode 100644
index 0000000..395bc2a
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/targetapp/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string translatable="false" name="test_string_in_target">Test string in target APK</string>
+</resources>
diff --git a/ravenwood/tests/bivalentinst/targetapp/src/com/android/ravenwoodtest/bivalentinst/Empty.java b/ravenwood/tests/bivalentinst/targetapp/src/com/android/ravenwoodtest/bivalentinst/Empty.java
new file mode 100644
index 0000000..15e50ec
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/targetapp/src/com/android/ravenwoodtest/bivalentinst/Empty.java
@@ -0,0 +1,22 @@
+/*
+ * 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.ravenwoodtest.bivalentinst;
+
+/**
+ * Empty class. We need it because an instrumentation target APK must have code.
+ */
+public class Empty {
+}
diff --git a/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_nonself.java b/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_nonself.java
new file mode 100644
index 0000000..ed1992c
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_nonself.java
@@ -0,0 +1,113 @@
+/*
+ * 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.ravenwoodtest.bivalentinst;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodConfig;
+import android.platform.test.ravenwood.RavenwoodConfig.Config;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the case where the instrumentation target is _not_ the test APK itself.
+ */
+@RunWith(AndroidJUnit4.class)
+@DisabledOnRavenwood(reason="AOSP is missing resources support")
+public class RavenwoodInstrumentationTest_nonself {
+ private static final String TARGET_PACKAGE_NAME =
+ "com.android.ravenwood.bivalentinst_target_app";
+ private static final String TEST_PACKAGE_NAME =
+ "com.android.ravenwood.bivalentinsttest_nonself_inst";
+
+ @Config
+ public static final RavenwoodConfig sConfig = new RavenwoodConfig.Builder()
+ .setPackageName(TEST_PACKAGE_NAME)
+ .setTargetPackageName(TARGET_PACKAGE_NAME)
+ .build();
+
+ private static Instrumentation sInstrumentation;
+ private static Context sTestContext;
+ private static Context sTargetContext;
+
+ @BeforeClass
+ public static void beforeClass() {
+ sInstrumentation = InstrumentationRegistry.getInstrumentation();
+ sTestContext = sInstrumentation.getContext();
+ sTargetContext = sInstrumentation.getTargetContext();
+ }
+
+ @Test
+ public void testTestContextPackageName() {
+ assertThat(sTestContext.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ }
+
+ @Test
+ public void testTargetContextPackageName() {
+ assertThat(sTargetContext.getPackageName()).isEqualTo(TARGET_PACKAGE_NAME);
+ }
+
+ @Test
+ public void testTestAppContext() {
+ // Test context doesn't have an app context.
+ assertThat(sTestContext.getApplicationContext()).isNull();
+ }
+
+ @Test
+ public void testTargetAppContextPackageName() {
+ assertThat(sTargetContext.getApplicationContext().getPackageName())
+ .isEqualTo(TARGET_PACKAGE_NAME);
+ }
+
+ @Test
+ public void testTargetAppAppContextPackageName() {
+ assertThat(sTargetContext.getApplicationContext()
+ .getApplicationContext().getPackageName())
+ .isEqualTo(TARGET_PACKAGE_NAME);
+ }
+
+ @Test
+ public void testContextSameness() {
+ assertThat(sTargetContext).isNotSameInstanceAs(sTestContext);
+
+ assertThat(sTargetContext).isNotSameInstanceAs(sTargetContext.getApplicationContext());
+
+ assertThat(sTargetContext.getApplicationContext()).isSameInstanceAs(
+ sTargetContext.getApplicationContext().getApplicationContext());
+ }
+
+ @Test
+ public void testTargetAppResource() {
+ assertThat(sTargetContext.getString(
+ com.android.ravenwood.bivalentinst_target_app.R.string.test_string_in_target))
+ .isEqualTo("Test string in target APK");
+ }
+
+ @Test
+ public void testTestAppResource() {
+ assertThat(sTestContext.getString(
+ com.android.ravenwood.bivalentinsttest_nonself_inst.R.string.test_string_in_test))
+ .isEqualTo("String in test APK");
+ }
+}
diff --git a/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_self.java b/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_self.java
new file mode 100644
index 0000000..b5bafc4
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_self.java
@@ -0,0 +1,125 @@
+/*
+ * 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.ravenwoodtest.bivalentinst;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodConfig;
+import android.platform.test.ravenwood.RavenwoodConfig.Config;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the case where the instrumentation target is the test APK itself.
+ */
+@RunWith(AndroidJUnit4.class)
+@DisabledOnRavenwood(reason="AOSP is missing resources support")
+public class RavenwoodInstrumentationTest_self {
+
+ private static final String TARGET_PACKAGE_NAME =
+ "com.android.ravenwood.bivalentinsttest_self_inst";
+ private static final String TEST_PACKAGE_NAME =
+ "com.android.ravenwood.bivalentinsttest_self_inst";
+
+ @Config
+ public static final RavenwoodConfig sConfig = new RavenwoodConfig.Builder()
+ .setPackageName(TEST_PACKAGE_NAME)
+ .setTargetPackageName(TARGET_PACKAGE_NAME)
+ .build();
+
+
+ private static Instrumentation sInstrumentation;
+ private static Context sTestContext;
+ private static Context sTargetContext;
+
+ @BeforeClass
+ public static void beforeClass() {
+ sInstrumentation = InstrumentationRegistry.getInstrumentation();
+ sTestContext = sInstrumentation.getContext();
+ sTargetContext = sInstrumentation.getTargetContext();
+ }
+
+ @Test
+ public void testTestContextPackageName() {
+ assertThat(sTestContext.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ }
+
+ @Test
+ public void testTargetContextPackageName() {
+ assertThat(sTargetContext.getPackageName()).isEqualTo(TARGET_PACKAGE_NAME);
+ }
+
+ @Test
+ public void testTestAppContextPackageName() {
+ assertThat(sTestContext.getApplicationContext().getPackageName())
+ .isEqualTo(TEST_PACKAGE_NAME);
+ }
+
+ @Test
+ public void testTestAppAppContextPackageName() {
+ assertThat(sTestContext.getApplicationContext().getPackageName())
+ .isEqualTo(TEST_PACKAGE_NAME);
+ }
+
+ @Test
+ public void testTargetAppContextPackageName() {
+ assertThat(sTargetContext.getApplicationContext()
+ .getApplicationContext().getPackageName())
+ .isEqualTo(TARGET_PACKAGE_NAME);
+ }
+
+ @Test
+ public void testTargetAppAppContextPackageName() {
+ assertThat(sTargetContext.getApplicationContext()
+ .getApplicationContext().getPackageName())
+ .isEqualTo(TARGET_PACKAGE_NAME);
+ }
+
+ @Test
+ public void testContextSameness() {
+ assertThat(sTargetContext).isNotSameInstanceAs(sTestContext);
+
+ assertThat(sTestContext).isNotSameInstanceAs(sTestContext.getApplicationContext());
+ assertThat(sTargetContext).isNotSameInstanceAs(sTargetContext.getApplicationContext());
+
+ assertThat(sTestContext.getApplicationContext()).isSameInstanceAs(
+ sTestContext.getApplicationContext().getApplicationContext());
+ assertThat(sTargetContext.getApplicationContext()).isSameInstanceAs(
+ sTargetContext.getApplicationContext().getApplicationContext());
+ }
+
+ @Test
+ public void testTargetAppResource() {
+ assertThat(sTargetContext.getString(
+ com.android.ravenwood.bivalentinsttest_self_inst.R.string.test_string_in_test))
+ .isEqualTo("String in test APK");
+ }
+
+ @Test
+ public void testTestAppResource() {
+ assertThat(sTestContext.getString(
+ com.android.ravenwood.bivalentinsttest_self_inst.R.string.test_string_in_test))
+ .isEqualTo("String in test APK");
+ }
+}
diff --git a/ravenwood/coretest/Android.bp b/ravenwood/tests/coretest/Android.bp
similarity index 87%
rename from ravenwood/coretest/Android.bp
rename to ravenwood/tests/coretest/Android.bp
index a78c5c1..d94475c 100644
--- a/ravenwood/coretest/Android.bp
+++ b/ravenwood/tests/coretest/Android.bp
@@ -14,10 +14,12 @@
"androidx.annotation_annotation",
"androidx.test.ext.junit",
"androidx.test.rules",
+ "junit-params",
+ "platform-parametric-runner-lib",
+ "truth",
],
srcs: [
"test/**/*.java",
],
- sdk_version: "test_current",
auto_gen_config: true,
}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerCallbackTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerCallbackTest.java
new file mode 100644
index 0000000..bd01313
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerCallbackTest.java
@@ -0,0 +1,458 @@
+/*
+ * 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.ravenwoodtest.runnercallbacktests;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.annotations.NoRavenizer;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+
+/**
+ * Tests to make sure {@link RavenwoodAwareTestRunner} produces expected callbacks in various
+ * error situations in places such as @BeforeClass / @AfterClass / Constructors, which are
+ * out of test method bodies.
+ */
+@NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner.
+public class RavenwoodRunnerCallbackTest extends RavenwoodRunnerTestBase {
+
+ /**
+ * Throws an exception in @AfterClass. This should produce a critical error.
+ */
+ @RunWith(BlockJUnit4ClassRunner.class)
+ // CHECKSTYLE:OFF Generated code
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$AfterClassFailureTest
+ testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$AfterClassFailureTest)
+ testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$AfterClassFailureTest)
+ testStarted: test2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$AfterClassFailureTest)
+ testFinished: test2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$AfterClassFailureTest)
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$AfterClassFailureTest
+ criticalError: Failures detected in @AfterClass, which would be swallowed by tradefed: FAILURE
+ testSuiteFinished: classes
+ testRunFinished: 2,0,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class AfterClassFailureTest {
+ public AfterClassFailureTest() {
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ throw new RuntimeException("FAILURE");
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+
+ }
+
+ /**
+ * Assumption failure in @BeforeClass.
+ */
+ @RunWith(ParameterizedAndroidJunit4.class)
+ // Because the test uses ParameterizedAndroidJunit4 with two parameters,
+ // the whole class is executed twice.
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest
+ testSuiteStarted: [0]
+ testStarted: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+ testAssumptionFailure: got: <false>, expected: is <true>
+ testFinished: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+ testStarted: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+ testAssumptionFailure: got: <false>, expected: is <true>
+ testFinished: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+ testSuiteFinished: [0]
+ testSuiteStarted: [1]
+ testStarted: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+ testAssumptionFailure: got: <false>, expected: is <true>
+ testFinished: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+ testStarted: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+ testAssumptionFailure: got: <false>, expected: is <true>
+ testFinished: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+ testSuiteFinished: [1]
+ testSuiteFinished: classes
+ testRunFinished: 4,0,4,0
+ """)
+ // CHECKSTYLE:ON
+ public static class BeforeClassAssumptionFailureTest {
+ public BeforeClassAssumptionFailureTest(String param) {
+ }
+
+ @BeforeClass
+ public static void beforeClass() {
+ Assume.assumeTrue(false);
+ }
+
+ @Parameters
+ public static List<String> getParams() {
+ var params = new ArrayList<String>();
+ params.add("foo");
+ params.add("bar");
+ return params;
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+ }
+
+ /**
+ * General exception in @BeforeClass.
+ */
+ @RunWith(ParameterizedAndroidJunit4.class)
+ // Because the test uses ParameterizedAndroidJunit4 with two parameters,
+ // the whole class is executed twice.
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest
+ testSuiteStarted: [0]
+ testStarted: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+ testFailure: FAILURE
+ testFinished: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+ testStarted: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+ testFailure: FAILURE
+ testFinished: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+ testSuiteFinished: [0]
+ testSuiteStarted: [1]
+ testStarted: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+ testFailure: FAILURE
+ testFinished: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+ testStarted: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+ testFailure: FAILURE
+ testFinished: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+ testSuiteFinished: [1]
+ testSuiteFinished: classes
+ testRunFinished: 4,4,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class BeforeClassExceptionTest {
+ public BeforeClassExceptionTest(String param) {
+ }
+
+ @BeforeClass
+ public static void beforeClass() {
+ throw new RuntimeException("FAILURE");
+ }
+
+ @Parameters
+ public static List<String> getParams() {
+ var params = new ArrayList<String>();
+ params.add("foo");
+ params.add("bar");
+ return params;
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+ }
+
+ /**
+ * Assumption failure from a @ClassRule.
+ */
+ @RunWith(ParameterizedAndroidJunit4.class)
+ // Because the test uses ParameterizedAndroidJunit4 with two parameters,
+ // the whole class is executed twice.
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest
+ testSuiteStarted: [0]
+ testStarted: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+ testAssumptionFailure: got: <false>, expected: is <true>
+ testFinished: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+ testStarted: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+ testAssumptionFailure: got: <false>, expected: is <true>
+ testFinished: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+ testSuiteFinished: [0]
+ testSuiteStarted: [1]
+ testStarted: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+ testAssumptionFailure: got: <false>, expected: is <true>
+ testFinished: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+ testStarted: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+ testAssumptionFailure: got: <false>, expected: is <true>
+ testFinished: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+ testSuiteFinished: [1]
+ testSuiteFinished: classes
+ testRunFinished: 4,0,4,0
+ """)
+ // CHECKSTYLE:ON
+ public static class ClassRuleAssumptionFailureTest {
+ @ClassRule
+ public static final TestRule sClassRule = new TestRule() {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ assumeTrue(false);
+ return null; // unreachable
+ }
+ };
+
+ public ClassRuleAssumptionFailureTest(String param) {
+ }
+
+ @Parameters
+ public static List<String> getParams() {
+ var params = new ArrayList<String>();
+ params.add("foo");
+ params.add("bar");
+ return params;
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+ }
+
+ /**
+ * General exception from a @ClassRule.
+ */
+ @RunWith(ParameterizedAndroidJunit4.class)
+ // Because the test uses ParameterizedAndroidJunit4 with two parameters,
+ // the whole class is executed twice.
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest
+ testSuiteStarted: [0]
+ testStarted: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+ testAssumptionFailure: got: <false>, expected: is <true>
+ testFinished: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+ testStarted: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+ testAssumptionFailure: got: <false>, expected: is <true>
+ testFinished: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+ testSuiteFinished: [0]
+ testSuiteStarted: [1]
+ testStarted: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+ testAssumptionFailure: got: <false>, expected: is <true>
+ testFinished: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+ testStarted: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+ testAssumptionFailure: got: <false>, expected: is <true>
+ testFinished: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+ testSuiteFinished: [1]
+ testSuiteFinished: classes
+ testRunFinished: 4,0,4,0
+ """)
+ // CHECKSTYLE:ON
+ public static class ClassRuleExceptionTest {
+ @ClassRule
+ public static final TestRule sClassRule = new TestRule() {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ assumeTrue(false);
+ return null; // unreachable
+ }
+ };
+
+ public ClassRuleExceptionTest(String param) {
+ }
+
+ @Parameters
+ public static List<String> getParams() {
+ var params = new ArrayList<String>();
+ params.add("foo");
+ params.add("bar");
+ return params;
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+ }
+
+ /**
+ * General exception from a @ClassRule.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testStarted: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ExceptionFromInnerRunnerConstructorTest)
+ testFailure: Exception detected in constructor
+ testFinished: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ExceptionFromInnerRunnerConstructorTest)
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class ExceptionFromInnerRunnerConstructorTest {
+ public ExceptionFromInnerRunnerConstructorTest(String arg1, String arg2) {
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+ }
+
+ /**
+ * The test class is unloadable, but has a @DisabledOnRavenwood.
+ */
+ @RunWith(AndroidJUnit4.class)
+ @DisabledOnRavenwood
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: ClassUnloadbleAndDisabledTest(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndDisabledTest)
+ testIgnored: ClassUnloadbleAndDisabledTest(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndDisabledTest)
+ testSuiteFinished: ClassUnloadbleAndDisabledTest(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndDisabledTest)
+ testSuiteFinished: classes
+ testRunFinished: 0,0,0,1
+ """)
+ // CHECKSTYLE:ON
+ public static class ClassUnloadbleAndDisabledTest {
+ static {
+ Assert.fail("Class unloadable!");
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+ }
+
+ /**
+ * The test class is unloadable, but has a @DisabledOnRavenwood.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndEnabledTest
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndEnabledTest
+ testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndEnabledTest)
+ testFailure: Class unloadable!
+ testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndEnabledTest)
+ testStarted: test2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndEnabledTest)
+ testFailure: Class unloadable!
+ testFinished: test2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndEnabledTest)
+ testSuiteFinished: classes
+ testRunFinished: 2,2,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class ClassUnloadbleAndEnabledTest {
+ static {
+ Assert.fail("Class unloadable!");
+ }
+
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+ }
+
+ public static class BrokenTestRunner extends BlockJUnit4ClassRunner {
+ public BrokenTestRunner(Class<?> testClass) throws InitializationError {
+ super(testClass);
+
+ if (true) {
+ throw new RuntimeException("This is a broken test runner!");
+ }
+ }
+ }
+
+ /**
+ * The test runner throws an exception from the ctor.
+ */
+ @RunWith(BrokenTestRunner.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testStarted: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BrokenRunnerTest)
+ testFailure: Exception detected in constructor
+ testFinished: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BrokenRunnerTest)
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class BrokenRunnerTest {
+ @Test
+ public void test1() {
+ }
+
+ @Test
+ public void test2() {
+ }
+ }
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java
new file mode 100644
index 0000000..73ea64f
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java
@@ -0,0 +1,481 @@
+/*
+ * 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.ravenwoodtest.runnercallbacktests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.NoRavenizer;
+import android.platform.test.ravenwood.RavenwoodConfig;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Test for @Config field extraction and validation.
+ */
+@NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner.
+public class RavenwoodRunnerConfigValidationTest extends RavenwoodRunnerTestBase {
+ public abstract static class ConfigInBaseClass {
+ static String PACKAGE_NAME = "com.ConfigInBaseClass";
+
+ @RavenwoodConfig.Config
+ public static RavenwoodConfig sConfig = new RavenwoodConfig.Builder()
+ .setPackageName(PACKAGE_NAME).build();
+ }
+
+ /**
+ * Make sure a config in the base class is detected.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigInBaseClassTest
+ testStarted: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigInBaseClassTest)
+ testFinished: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigInBaseClassTest)
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigInBaseClassTest
+ testSuiteFinished: classes
+ testRunFinished: 1,0,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class ConfigInBaseClassTest extends ConfigInBaseClass {
+ @Test
+ public void test() {
+ assertThat(InstrumentationRegistry.getInstrumentation().getContext().getPackageName())
+ .isEqualTo(PACKAGE_NAME);
+ }
+ }
+
+ /**
+ * Make sure a config in the base class is detected.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigOverridingTest
+ testStarted: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigOverridingTest)
+ testFinished: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigOverridingTest)
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigOverridingTest
+ testSuiteFinished: classes
+ testRunFinished: 1,0,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class ConfigOverridingTest extends ConfigInBaseClass {
+ static String PACKAGE_NAME_OVERRIDE = "com.ConfigOverridingTest";
+
+ @RavenwoodConfig.Config
+ public static RavenwoodConfig sConfig = new RavenwoodConfig.Builder()
+ .setPackageName(PACKAGE_NAME_OVERRIDE).build();
+
+ @Test
+ public void test() {
+ assertThat(InstrumentationRegistry.getInstrumentation().getContext().getPackageName())
+ .isEqualTo(PACKAGE_NAME_OVERRIDE);
+ }
+ }
+
+ /**
+ * Test to make sure that if a test has a config error, the failure would be reported from
+ * each test method.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testStarted: testMethod1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+ testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest.sConfig expected to be public static
+ testFinished: testMethod1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+ testStarted: testMethod2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+ testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest.sConfig expected to be public static
+ testFinished: testMethod2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+ testStarted: testMethod3(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+ testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest.sConfig expected to be public static
+ testFinished: testMethod3(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+ testSuiteFinished: classes
+ testRunFinished: 3,3,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class ErrorMustBeReportedFromEachTest {
+ @RavenwoodConfig.Config
+ private static RavenwoodConfig sConfig = // Invalid because it's private.
+ new RavenwoodConfig.Builder().build();
+
+ @Test
+ public void testMethod1() {
+ }
+
+ @Test
+ public void testMethod2() {
+ }
+
+ @Test
+ public void testMethod3() {
+ }
+ }
+
+ /**
+ * Invalid because there are two @Config's.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateConfigTest)
+ testFailure: Class com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest.DuplicateConfigTest has multiple fields with @RavenwoodConfig.Config
+ testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateConfigTest)
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class DuplicateConfigTest {
+
+ @RavenwoodConfig.Config
+ public static RavenwoodConfig sConfig1 =
+ new RavenwoodConfig.Builder().build();
+
+ @RavenwoodConfig.Config
+ public static RavenwoodConfig sConfig2 =
+ new RavenwoodConfig.Builder().build();
+
+ @Test
+ public void testConfig() {
+ }
+ }
+
+ /**
+ * @Config's must be static.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest)
+ testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest.sConfig expected to be public static
+ testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest)
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class NonStaticConfigTest {
+
+ @RavenwoodConfig.Config
+ public RavenwoodConfig sConfig =
+ new RavenwoodConfig.Builder().build();
+
+ @Test
+ public void testConfig() {
+ }
+ }
+
+ /**
+ * @Config's must be public.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest)
+ testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest.sConfig expected to be public static
+ testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest)
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class NonPublicConfigTest {
+
+ @RavenwoodConfig.Config
+ RavenwoodConfig sConfig =
+ new RavenwoodConfig.Builder().build();
+
+ @Test
+ public void testConfig() {
+ }
+ }
+
+ /**
+ * @Config's must be of type RavenwoodConfig.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeConfigTest)
+ testFailure: Field com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest.WrongTypeConfigTest.sConfig has @RavenwoodConfig.Config but type is not RavenwoodConfig
+ testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeConfigTest)
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class WrongTypeConfigTest {
+
+ @RavenwoodConfig.Config
+ public static Object sConfig =
+ new RavenwoodConfig.Builder().build();
+
+ @Test
+ public void testConfig() {
+ }
+
+ }
+
+ /**
+ * @Rule must be of type RavenwoodRule.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeRuleTest
+ testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeRuleTest)
+ testFailure: If you have a RavenwoodRule in your test, make sure the field type is RavenwoodRule so Ravenwood can detect it.
+ testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeRuleTest)
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeRuleTest
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class WrongTypeRuleTest {
+
+ @Rule
+ public TestRule mRule = new RavenwoodRule.Builder().build();
+
+ @Test
+ public void testConfig() {
+ }
+
+ }
+
+ /**
+ * Config can't be used with a (instance) Rule.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithInstanceRuleTest)
+ testFailure: RavenwoodConfig and RavenwoodRule cannot be used in the same class. Suggest migrating to RavenwoodConfig.
+ testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithInstanceRuleTest)
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class WithInstanceRuleTest {
+
+ @RavenwoodConfig.Config
+ public static RavenwoodConfig sConfig =
+ new RavenwoodConfig.Builder().build();
+
+ @Rule
+ public RavenwoodRule mRule = new RavenwoodRule.Builder().build();
+
+ @Test
+ public void testConfig() {
+ }
+ }
+
+ /**
+ * Config can't be used with a (static) Rule.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testStarted: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithStaticRuleTest)
+ testFailure: Exception detected in constructor
+ testFinished: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithStaticRuleTest)
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class WithStaticRuleTest {
+
+ @RavenwoodConfig.Config
+ public static RavenwoodConfig sConfig =
+ new RavenwoodConfig.Builder().build();
+
+ @Rule
+ public static RavenwoodRule sRule = new RavenwoodRule.Builder().build();
+
+ @Test
+ public void testConfig() {
+ }
+ }
+
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateRulesTest
+ testStarted: testMultipleRulesNotAllowed(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateRulesTest)
+ testFailure: Multiple nesting RavenwoodRule's are detected in the same class, which is not supported.
+ testFinished: testMultipleRulesNotAllowed(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateRulesTest)
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateRulesTest
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class DuplicateRulesTest {
+
+ @Rule
+ public final RavenwoodRule mRavenwood1 = new RavenwoodRule();
+
+ @Rule
+ public final RavenwoodRule mRavenwood2 = new RavenwoodRule();
+
+ @Test
+ public void testMultipleRulesNotAllowed() {
+ }
+ }
+
+ public static class RuleInBaseClass {
+ static String PACKAGE_NAME = "com.RuleInBaseClass";
+ @Rule
+ public final RavenwoodRule mRavenwood1 = new RavenwoodRule.Builder()
+ .setPackageName(PACKAGE_NAME).build();
+ }
+
+ /**
+ * Make sure that RavenwoodRule in a base class takes effect.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleInBaseClassSuccessTest
+ testStarted: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleInBaseClassSuccessTest)
+ testFinished: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleInBaseClassSuccessTest)
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleInBaseClassSuccessTest
+ testSuiteFinished: classes
+ testRunFinished: 1,0,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class RuleInBaseClassSuccessTest extends RuleInBaseClass {
+
+ @Test
+ public void testRuleInBaseClass() {
+ assertThat(InstrumentationRegistry.getInstrumentation().getContext().getPackageName())
+ .isEqualTo(PACKAGE_NAME);
+ }
+ }
+
+ /**
+ * Make sure that having a config and a rule in a base class should fail.
+ * RavenwoodRule.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testStarted: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleInBaseClassTest)
+ testFailure: RavenwoodConfig and RavenwoodRule cannot be used in the same class. Suggest migrating to RavenwoodConfig.
+ testFinished: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleInBaseClassTest)
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class ConfigWithRuleInBaseClassTest extends RuleInBaseClass {
+ @RavenwoodConfig.Config
+ public static RavenwoodConfig sConfig = new RavenwoodConfig.Builder().build();
+
+ @Test
+ public void test() {
+ }
+ }
+
+ /**
+ * Same as {@link RuleInBaseClass}, but the type of the rule field is not {@link RavenwoodRule}.
+ */
+ public abstract static class RuleWithDifferentTypeInBaseClass {
+ static String PACKAGE_NAME = "com.RuleWithDifferentTypeInBaseClass";
+ @Rule
+ public final TestRule mRavenwood1 = new RavenwoodRule.Builder()
+ .setPackageName(PACKAGE_NAME).build();
+ }
+
+ /**
+ * Make sure that RavenwoodRule in a base class takes effect, even if the field type is not
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest
+ testStarted: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest)
+ testFailure: If you have a RavenwoodRule in your test, make sure the field type is RavenwoodRule so Ravenwood can detect it.
+ testFinished: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest)
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class RuleWithDifferentTypeInBaseClassSuccessTest extends RuleWithDifferentTypeInBaseClass {
+
+ @Test
+ public void testRuleInBaseClass() {
+ assertThat(InstrumentationRegistry.getInstrumentation().getContext().getPackageName())
+ .isEqualTo(PACKAGE_NAME);
+ }
+ }
+
+ /**
+ * Make sure that having a config and a rule in a base class should fail, even if the field type is not
+ * RavenwoodRule.
+ */
+ @RunWith(AndroidJUnit4.class)
+ // CHECKSTYLE:OFF
+ @Expected("""
+ testRunStarted: classes
+ testSuiteStarted: classes
+ testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleWithDifferentTypeInBaseClassTest
+ testStarted: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleWithDifferentTypeInBaseClassTest)
+ testFailure: If you have a RavenwoodRule in your test, make sure the field type is RavenwoodRule so Ravenwood can detect it.
+ testFinished: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleWithDifferentTypeInBaseClassTest)
+ testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleWithDifferentTypeInBaseClassTest
+ testSuiteFinished: classes
+ testRunFinished: 1,1,0,0
+ """)
+ // CHECKSTYLE:ON
+ public static class ConfigWithRuleWithDifferentTypeInBaseClassTest extends RuleWithDifferentTypeInBaseClass {
+ @RavenwoodConfig.Config
+ public static RavenwoodConfig sConfig = new RavenwoodConfig.Builder().build();
+
+ @Test
+ public void test() {
+ }
+ }
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerTestBase.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerTestBase.java
new file mode 100644
index 0000000..9a6934bf
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerTestBase.java
@@ -0,0 +1,220 @@
+/*
+ * 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.ravenwoodtest.runnercallbacktests;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.platform.test.annotations.NoRavenizer;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner;
+import android.util.Log;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.JUnitCore;
+import org.junit.runner.Result;
+import org.junit.runner.RunWith;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.function.BiConsumer;
+
+
+/**
+ * Base class for tests to make sure {@link RavenwoodAwareTestRunner} produces expected callbacks
+ * in various situations. (most of them are error situations.)
+ *
+ * Subclasses must contain test classes as static inner classes with an {@link Expected} annotation.
+ * This class finds them using reflections and run them one by one directly using {@link JUnitCore},
+ * and check the callbacks.
+ *
+ * Subclasses do no need to have any test methods.
+ *
+ * The {@link Expected} annotation must contain the expected result as a string.
+ *
+ * This test abuses the fact that atest + tradefed + junit won't run nested classes automatically.
+ * (So atest won't show any results directly from the nested classes.)
+ *
+ * The actual test method is {@link #doTest}, which is executed for each target test class, using
+ * junit-params.
+ */
+@RunWith(JUnitParamsRunner.class)
+@NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner.
+public abstract class RavenwoodRunnerTestBase {
+ private static final String TAG = "RavenwoodRunnerTestBase";
+
+ /**
+ * Annotation to specify the expected result for a class.
+ */
+ @Target({ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface Expected {
+ String value();
+ }
+
+ /**
+ * Take a multiline string, strip all of them, remove empty lines, and return it.
+ */
+ private static String stripMultiLines(String resultString) {
+ var list = new ArrayList<String>();
+ for (var line : resultString.split("\n")) {
+ var s = line.strip();
+ if (s.length() > 0) {
+ list.add(s);
+ }
+ }
+ return String.join("\n", list);
+ }
+
+ /**
+ * Extract the expected result from @Expected.
+ */
+ private String getExpectedResult(Class<?> testClazz) {
+ var expect = testClazz.getAnnotation(Expected.class);
+ return stripMultiLines(expect.value());
+ }
+
+ /**
+ * List all the nested classrs with an {@link Expected} annotation in a given class.
+ */
+ public Class<?>[] getTestClasses() {
+ var thisClass = this.getClass();
+ var ret = Arrays.stream(thisClass.getNestMembers())
+ .filter((c) -> c.getAnnotation(Expected.class) != null)
+ .toArray(Class[]::new);
+
+ assertThat(ret.length).isGreaterThan(0);
+
+ return ret;
+ }
+
+ /**
+ * This is the actual test method. We use junit-params to run this method for each target
+ * test class, which are returned by {@link #getTestClasses}.
+ *
+ * It runs each test class, and compare the result collected with
+ * {@link ResultCollectingListener} to expected results (as strings).
+ */
+ @Test
+ @Parameters(method = "getTestClasses")
+ public void doTest(Class<?> testClazz) {
+ doTest(testClazz, getExpectedResult(testClazz));
+ }
+
+ /**
+ * Run a given test class, and compare the result collected with
+ * {@link ResultCollectingListener} to expected results (as a string).
+ */
+ private void doTest(Class<?> testClazz, String expectedResult) {
+ Log.i(TAG, "Running test for " + testClazz);
+ var junitCore = new JUnitCore();
+
+ // Create a listener.
+ var listener = new ResultCollectingListener();
+ junitCore.addListener(listener);
+
+ // Set a listener to critical errors. This will also prevent
+ // {@link RavenwoodAwareTestRunner} from calling System.exit() when there's
+ // a critical error.
+ RavenwoodAwareTestRunner.private$ravenwood().setCriticalErrorHandler(
+ listener.sCriticalErrorListener);
+
+ try {
+ // Run the test class.
+ junitCore.run(testClazz);
+ } finally {
+ // Clear the critical error listener.
+ RavenwoodAwareTestRunner.private$ravenwood().setCriticalErrorHandler(null);
+ }
+
+ // Check the result.
+ assertWithMessage("Failure in test class: " + testClazz.getCanonicalName() + "]")
+ .that(listener.getResult())
+ .isEqualTo(expectedResult);
+ }
+
+ /**
+ * A JUnit RunListener that collects all the callbacks as a single string.
+ */
+ private static class ResultCollectingListener extends RunListener {
+ private final ArrayList<String> mResult = new ArrayList<>();
+
+ public final BiConsumer<String, Throwable> sCriticalErrorListener = (message, th) -> {
+ mResult.add("criticalError: " + message + ": " + th.getMessage());
+ };
+
+ @Override
+ public void testRunStarted(Description description) throws Exception {
+ mResult.add("testRunStarted: " + description);
+ }
+
+ @Override
+ public void testRunFinished(Result result) throws Exception {
+ mResult.add("testRunFinished: "
+ + result.getRunCount() + ","
+ + result.getFailureCount() + ","
+ + result.getAssumptionFailureCount() + ","
+ + result.getIgnoreCount());
+ }
+
+ @Override
+ public void testSuiteStarted(Description description) throws Exception {
+ mResult.add("testSuiteStarted: " + description);
+ }
+
+ @Override
+ public void testSuiteFinished(Description description) throws Exception {
+ mResult.add("testSuiteFinished: " + description);
+ }
+
+ @Override
+ public void testStarted(Description description) throws Exception {
+ mResult.add("testStarted: " + description);
+ }
+
+ @Override
+ public void testFinished(Description description) throws Exception {
+ mResult.add("testFinished: " + description);
+ }
+
+ @Override
+ public void testFailure(Failure failure) throws Exception {
+ mResult.add("testFailure: " + failure.getException().getMessage());
+ }
+
+ @Override
+ public void testAssumptionFailure(Failure failure) {
+ mResult.add("testAssumptionFailure: " + failure.getException().getMessage());
+ }
+
+ @Override
+ public void testIgnored(Description description) throws Exception {
+ mResult.add("testIgnored: " + description);
+ }
+
+ public String getResult() {
+ return String.join("\n", mResult);
+ }
+ }
+}
diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
index f3172ae..9c86389 100644
--- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
@@ -9,17 +9,13 @@
com.android.internal.logging.testing.UiEventLoggerFake
com.android.internal.os.AndroidPrintStream
com.android.internal.os.BatteryStatsHistory
-com.android.internal.os.BatteryStatsHistory$TraceDelegate
-com.android.internal.os.BatteryStatsHistory$VarintParceler
com.android.internal.os.BatteryStatsHistoryIterator
com.android.internal.os.Clock
com.android.internal.os.LongArrayMultiStateCounter
-com.android.internal.os.LongArrayMultiStateCounter$LongArrayContainer
com.android.internal.os.LongMultiStateCounter
com.android.internal.os.MonotonicClock
com.android.internal.os.PowerProfile
com.android.internal.os.PowerStats
-com.android.internal.os.PowerStats$Descriptor
com.android.internal.os.RuntimeInit
com.android.internal.power.EnergyConsumerStats
com.android.internal.power.ModemPowerProfile
@@ -39,12 +35,14 @@
android.util.DataUnit
android.util.DayOfMonthCursor
android.util.DebugUtils
+android.util.DisplayMetrics
android.util.Dumpable
android.util.DumpableContainer
android.util.EmptyArray
android.util.EventLog
android.util.FloatProperty
android.util.FloatMath
+android.util.Half
android.util.IndentingPrintWriter
android.util.IntArray
android.util.IntProperty
@@ -98,10 +96,12 @@
android.util.SparseIntArray
android.util.SparseLongArray
android.util.SparseSetArray
+android.util.StateSet
android.util.StringBuilderPrinter
android.util.TeeWriter
android.util.TimeUtils
android.util.TimingsTraceLog
+android.util.TypedValue
android.util.UtilConfig
android.util.Xml
@@ -119,15 +119,9 @@
android.os.BaseBundle
android.os.BatteryConsumer
android.os.BatteryStats
-android.os.BatteryStats$HistoryItem
-android.os.BatteryStats$HistoryStepDetails
-android.os.BatteryStats$HistoryTag
-android.os.BatteryStats$LongCounter
-android.os.BatteryStats$ProcessStateChange
android.os.BatteryUsageStats
android.os.BatteryUsageStatsQuery
android.os.Binder
-android.os.Binder$IdentitySupplier
android.os.BluetoothBatteryStats
android.os.Broadcaster
android.os.Build
@@ -138,19 +132,17 @@
android.os.DeadObjectException
android.os.DeadSystemException
android.os.FileUtils
-android.os.FileUtils$MemoryPipe
android.os.Handler
android.os.HandlerExecutor
android.os.HandlerThread
android.os.IBinder
+android.os.LocaleList
android.os.Looper
android.os.Message
android.os.MessageQueue
android.os.PackageTagsList
android.os.Parcel
android.os.ParcelFileDescriptor
-android.os.ParcelFileDescriptor$AutoCloseInputStream
-android.os.ParcelFileDescriptor$AutoCloseOutputStream
android.os.ParcelFormatException
android.os.ParcelUuid
android.os.Parcelable
@@ -164,7 +156,6 @@
android.os.RemoteException
android.os.ResultReceiver
android.os.ServiceManager
-android.os.ServiceManager$ServiceNotFoundException
android.os.ServiceSpecificException
android.os.StrictMode
android.os.SystemClock
@@ -175,23 +166,20 @@
android.os.Trace
android.os.TransactionTooLargeException
android.os.UserBatteryConsumer
-android.os.UserBatteryConsumer$Builder
android.os.UidBatteryConsumer
-android.os.UidBatteryConsumer$Builder
android.os.UserHandle
android.os.UserManager
android.os.VibrationAttributes
-android.os.VibrationAttributes$Builder
android.os.WakeLockStats
android.os.WorkSource
android.content.ClipData
-android.content.ClipData$Item
android.content.ClipDescription
android.content.ClipboardManager
android.content.ComponentName
android.content.ContentUris
android.content.ContentValues
+android.content.Context
android.content.ContextWrapper
android.content.Intent
android.content.IntentFilter
@@ -202,11 +190,7 @@
android.content.pm.ComponentInfo
android.content.pm.PackageInfo
android.content.pm.PackageItemInfo
-android.content.pm.PackageManager$Flags
-android.content.pm.PackageManager$PackageInfoFlags
-android.content.pm.PackageManager$ApplicationInfoFlags
-android.content.pm.PackageManager$ComponentInfoFlags
-android.content.pm.PackageManager$ResolveInfoFlags
+android.content.pm.PackageManager
android.content.pm.PathPermission
android.content.pm.ProviderInfo
android.content.pm.ResolveInfo
@@ -214,6 +198,32 @@
android.content.pm.Signature
android.content.pm.UserInfo
+android.content.res.ApkAssets
+android.content.res.AssetFileDescriptor
+android.content.res.AssetManager
+android.content.res.ColorStateList
+android.content.res.ConfigurationBoundResourceCache
+android.content.res.Configuration
+android.content.res.CompatibilityInfo
+android.content.res.ComplexColor
+android.content.res.ConstantState
+android.content.res.DrawableCache
+android.content.res.Element
+android.content.res.FontResourcesParser
+android.content.res.FontScaleConverter
+android.content.res.FontScaleConverterImpl
+android.content.res.FontScaleConverterFactory
+android.content.res.Resources
+android.content.res.ResourceId
+android.content.res.ResourcesImpl
+android.content.res.ResourcesKey
+android.content.res.StringBlock
+android.content.res.TagCounter
+android.content.res.ThemedResourceCache
+android.content.res.TypedArray
+android.content.res.Validator
+android.content.res.XmlBlock
+
android.database.AbstractCursor
android.database.CharArrayBuffer
android.database.ContentObservable
@@ -226,7 +236,6 @@
android.database.DataSetObservable
android.database.DataSetObserver
android.database.MatrixCursor
-android.database.MatrixCursor$RowBuilder
android.database.MergeCursor
android.database.Observable
android.database.SQLException
@@ -234,7 +243,6 @@
android.database.sqlite.SQLiteException
android.text.TextUtils
-android.text.TextUtils$SimpleStringSplitter
android.accounts.Account
@@ -244,7 +252,11 @@
android.graphics.Insets
android.graphics.Interpolator
android.graphics.Matrix
+android.graphics.Matrix44
+android.graphics.Outline
+android.graphics.ParcelableColorSpace
android.graphics.Path
+android.graphics.PixelFormat
android.graphics.Point
android.graphics.PointF
android.graphics.Rect
@@ -254,15 +266,18 @@
android.app.ActivityManager
android.app.ActivityOptions
+android.app.ApplicationPackageManager
android.app.BroadcastOptions
android.app.ComponentOptions
android.app.Instrumentation
+android.app.LocaleConfig
+android.app.ResourcesManager
+android.app.WindowConfiguration
android.metrics.LogMaker
android.view.Display
-android.view.Display$HdrCapabilities
-android.view.Display$Mode
+android.view.DisplayAdjustments
android.view.DisplayInfo
android.view.inputmethod.InputBinding
@@ -274,10 +289,18 @@
android.telephony.ModemActivityInfo
android.telephony.ServiceState
+android.os.connectivity.CellularBatteryStats
android.os.connectivity.WifiActivityEnergyInfo
+android.os.connectivity.WifiBatteryStats
com.android.server.LocalServices
+com.android.internal.graphics.cam.Cam
+com.android.internal.graphics.cam.CamUtils
+com.android.internal.graphics.cam.Frame
+com.android.internal.graphics.cam.HctSolver
+com.android.internal.graphics.ColorUtils
+
com.android.internal.util.BitUtils
com.android.internal.util.BitwiseInputStream
com.android.internal.util.BitwiseOutputStream
@@ -302,6 +325,7 @@
com.android.internal.util.QuickSelect
com.android.internal.util.RingBuffer
com.android.internal.util.SizedInputStream
+com.android.internal.util.RateLimitingCache
com.android.internal.util.StringPool
com.android.internal.util.TokenBucket
com.android.internal.util.XmlPullParserWrapper
diff --git a/ravenwood/texts/ravenwood-framework-policies.txt b/ravenwood/texts/ravenwood-framework-policies.txt
index 4012bdc..d962c82 100644
--- a/ravenwood/texts/ravenwood-framework-policies.txt
+++ b/ravenwood/texts/ravenwood-framework-policies.txt
@@ -9,11 +9,21 @@
# Keep all sysprops generated code implementations
class :sysprops keepclass
+# Keep all resource R classes
+class :r keepclass
+
# To avoid VerifyError on nano proto files (b/324063814), we rename nano proto classes.
-# Note: The "rename" directive must use shashes (/) as a package name separator.
+# Note: The "rename" directive must use slashes (/) as a package name separator.
rename com/.*/nano/ devicenano/
rename android/.*/nano/ devicenano/
+# Support APIs not available in standard JRE
+class java.io.FileDescriptor keep
+ method getInt$ ()I @com.android.ravenwood.RavenwoodJdkPatch.getInt$
+ method setInt$ (I)V @com.android.ravenwood.RavenwoodJdkPatch.setInt$
+class java.util.LinkedHashMap keep
+ method eldest ()Ljava/util/Map$Entry; @com.android.ravenwood.RavenwoodJdkPatch.eldest
+
# Exported to Mainline modules; cannot use annotations
class com.android.internal.util.FastXmlSerializer keepclass
class com.android.internal.util.FileRotator keepclass
@@ -62,3 +72,7 @@
method <init> ()V keep
class android.text.ClipboardManager keep
method <init> ()V keep
+
+# Just enough to allow ResourcesManager to run
+class android.hardware.display.DisplayManagerGlobal keep
+ method getInstance ()Landroid/hardware/display/DisplayManagerGlobal; ignore
diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt
index f64f26d..3ec3e3c 100644
--- a/ravenwood/texts/ravenwood-standard-options.txt
+++ b/ravenwood/texts/ravenwood-standard-options.txt
@@ -6,8 +6,6 @@
--default-throw
# Uncomment below lines to enable each feature.
-# --enable-non-stub-method-check
---no-non-stub-method-check
#--default-method-call-hook
# com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
@@ -34,8 +32,11 @@
--substitute-annotation
android.ravenwood.annotation.RavenwoodReplace
---native-substitute-annotation
- android.ravenwood.annotation.RavenwoodNativeSubstitutionClass
+--redirect-annotation
+ android.ravenwood.annotation.RavenwoodRedirect
+
+--redirection-class-annotation
+ android.ravenwood.annotation.RavenwoodRedirectionClass
--class-load-hook-annotation
android.ravenwood.annotation.RavenwoodClassLoadHook
diff --git a/ravenwood/tools/ravenizer/Android.bp b/ravenwood/tools/ravenizer/Android.bp
new file mode 100644
index 0000000..2892d07
--- /dev/null
+++ b/ravenwood/tools/ravenizer/Android.bp
@@ -0,0 +1,25 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_binary_host {
+ name: "ravenizer",
+ main_class: "com.android.platform.test.ravenwood.ravenizer.RavenizerMain",
+ srcs: ["src/**/*.kt"],
+ static_libs: [
+ "hoststubgen-lib",
+ "ow2-asm",
+ "ow2-asm-analysis",
+ "ow2-asm-commons",
+ "ow2-asm-tree",
+ "ow2-asm-util",
+ "junit",
+ "ravenwood-junit-impl-for-ravenizer",
+ ],
+ visibility: ["//visibility:public"],
+}
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
new file mode 100644
index 0000000..0dcd271
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Exceptions.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+@file:Suppress("ktlint:standard:filename")
+
+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
new file mode 100644
index 0000000..f7f9a85
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
@@ -0,0 +1,249 @@
+/*
+ * 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.GeneralUserErrorException
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.zipEntryNameToClassName
+import com.android.hoststubgen.executableName
+import com.android.hoststubgen.log
+import com.android.platform.test.ravenwood.ravenizer.adapter.RunnerRewritingAdapter
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.util.CheckClassAdapter
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.FileOutputStream
+import java.io.InputStream
+import java.io.OutputStream
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+import java.util.zip.ZipOutputStream
+
+/**
+ * Various stats on Ravenizer.
+ */
+data class RavenizerStats(
+ /** Total end-to-end time. */
+ var totalTime: Double = .0,
+
+ /** 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,
+
+ /** Total real time spent for converting class files (except for I/O time). */
+ var totalConversionTime: Double = .0,
+
+ /** Total real time spent for copying class files without modification. */
+ var totalCopyTime: Double = .0,
+
+ /** # of entries in the input jar file */
+ var totalEntiries: Int = 0,
+
+ /** # of *.class files in the input jar file */
+ var totalClasses: Int = 0,
+
+ /** # of *.class files that have been processed. */
+ var processedClasses: Int = 0,
+) {
+ override fun toString(): String {
+ return """
+ RavenizerStats{
+ totalTime=$totalTime,
+ loadStructureTime=$loadStructureTime,
+ validationTime=$validationTime,
+ totalProcessTime=$totalProcessTime,
+ totalConversionTime=$totalConversionTime,
+ totalCopyTime=$totalCopyTime,
+ totalEntiries=$totalEntiries,
+ totalClasses=$totalClasses,
+ processedClasses=$processedClasses,
+ }
+ """.trimIndent()
+ }
+}
+
+/**
+ * Main class.
+ */
+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,
+ options.enableValidation.get,
+ fatalValidation,
+ stats,
+ )
+ }
+ log.i(stats.toString())
+ }
+
+ 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.vTime("$executableName processing $inJar") {
+ ZipFile(inJar).use { inZip ->
+ val inEntries = inZip.entries()
+
+ stats.totalEntiries = inZip.size()
+
+ ZipOutputStream(BufferedOutputStream(FileOutputStream(outJar))).use { outZip ->
+ while (inEntries.hasMoreElements()) {
+ val entry = inEntries.nextElement()
+
+ if (entry.name.endsWith(".dex")) {
+ // Seems like it's an ART jar file. We can't process it.
+ // It's a fatal error.
+ throw GeneralUserErrorException(
+ "$inJar is not a desktop jar file. It contains a *.dex file."
+ )
+ }
+
+ val className = zipEntryNameToClassName(entry.name)
+
+ if (className != null) {
+ stats.totalClasses += 1
+ }
+
+ if (className != null && shouldProcessClass(allClasses, className)) {
+ stats.processedClasses += 1
+ processSingleClass(inZip, entry, outZip, allClasses, stats)
+ } else {
+ // Too slow, let's use merge_zips to bring back the original classes.
+ copyZipEntry(inZip, entry, outZip, stats)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Copy a single ZIP entry to the output.
+ */
+ private fun copyZipEntry(
+ inZip: ZipFile,
+ entry: ZipEntry,
+ out: ZipOutputStream,
+ stats: RavenizerStats,
+ ) {
+ stats.totalCopyTime += log.nTime {
+ inZip.getInputStream(entry).use { ins ->
+ // Copy unknown entries as is to the impl out. (but not to the stub out.)
+ val outEntry = ZipEntry(entry.name)
+ outEntry.method = 0
+ outEntry.size = entry.size
+ outEntry.crc = entry.crc
+ out.putNextEntry(outEntry)
+
+ ins.transferTo(out)
+
+ out.closeEntry()
+ }
+ }
+ }
+
+ private fun processSingleClass(
+ inZip: ZipFile,
+ entry: ZipEntry,
+ outZip: ZipOutputStream,
+ allClasses: ClassNodes,
+ stats: RavenizerStats,
+ ) {
+ val newEntry = ZipEntry(entry.name)
+ outZip.putNextEntry(newEntry)
+
+ BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
+ processSingleClass(entry, bis, outZip, allClasses, stats)
+ }
+ outZip.closeEntry()
+ }
+
+ /**
+ * Whether a class needs to be processed. This must be kept in sync with [processSingleClass].
+ */
+ private fun shouldProcessClass(classes: ClassNodes, classInternalName: String): Boolean {
+ return !classInternalName.shouldByBypassed()
+ && RunnerRewritingAdapter.shouldProcess(classes, classInternalName)
+ }
+
+ private fun processSingleClass(
+ entry: ZipEntry,
+ input: InputStream,
+ output: OutputStream,
+ allClasses: ClassNodes,
+ stats: RavenizerStats,
+ ) {
+ val cr = ClassReader(input)
+
+ lateinit var data: ByteArray
+ stats.totalConversionTime += log.vTime("Modify ${entry.name}") {
+
+ val classInternalName = zipEntryNameToClassName(entry.name)
+ ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}")
+ val flags = ClassWriter.COMPUTE_MAXS
+ val cw = ClassWriter(flags)
+ var outVisitor: ClassVisitor = cw
+
+ val enableChecker = false
+ if (enableChecker) {
+ outVisitor = CheckClassAdapter(outVisitor)
+ }
+
+ // This must be kept in sync with shouldProcessClass.
+ outVisitor = RunnerRewritingAdapter.maybeApply(
+ classInternalName, allClasses, outVisitor)
+
+ cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
+
+ data = cw.toByteArray()
+ }
+ output.write(data)
+ }
+}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
new file mode 100644
index 0000000..ff41818
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+@file:JvmName("RavenizerMain")
+
+package com.android.platform.test.ravenwood.ravenizer
+
+import com.android.hoststubgen.LogLevel
+import com.android.hoststubgen.executableName
+import com.android.hoststubgen.log
+import com.android.hoststubgen.runMainWithBoilerplate
+
+/**
+ * Entry point.
+ */
+fun main(args: Array<String>) {
+ executableName = "Ravenizer"
+ log.setConsoleLogLevel(LogLevel.Info)
+
+ runMainWithBoilerplate {
+ val options = RavenizerOptions.parseArgs(args)
+
+ log.i("$executableName started")
+ log.v("Options: $options")
+
+ // Run.
+ Ravenizer(options).run()
+ }
+}
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
new file mode 100644
index 0000000..10fe0a3
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.ArgIterator
+import com.android.hoststubgen.ArgumentsException
+import com.android.hoststubgen.SetOnce
+import com.android.hoststubgen.ensureFileExists
+import com.android.hoststubgen.log
+import java.nio.file.Paths
+import kotlin.io.path.exists
+
+/**
+ * If this file exits, we also read options from it. This is "unsafe" because it could break
+ * incremental builds, if it sets any flag that affects the output file.
+ * (however, for now, there's no such options.)
+ *
+ * For example, to enable verbose logging, do `echo '-v' > ~/.raveniezr-unsafe`
+ *
+ * (but even the content of this file changes, soong won't rerun the command, so you need to
+ * remove the output first and then do a build again.)
+ */
+private val RAVENIZER_DOTFILE = System.getenv("HOME") + "/.raveniezr-unsafe"
+
+class RavenizerOptions(
+ /** Input jar file*/
+ var inJar: SetOnce<String> = SetOnce(""),
+
+ /** 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(origArgs: Array<String>): RavenizerOptions {
+ val args = origArgs.toMutableList()
+ if (Paths.get(RAVENIZER_DOTFILE).exists()) {
+ log.i("Reading options from $RAVENIZER_DOTFILE")
+ args.add(0, "@$RAVENIZER_DOTFILE")
+ }
+
+ val ret = RavenizerOptions()
+ val ai = ArgIterator.withAtFiles(args.toTypedArray())
+
+ while (true) {
+ val arg = ai.nextArgOptional()
+ if (arg == null) {
+ break
+ }
+
+ fun nextArg(): String = ai.nextArgRequired(arg)
+
+ if (log.maybeHandleCommandLineArg(arg) { nextArg() }) {
+ continue
+ }
+ try {
+ when (arg) {
+ // TODO: Write help
+ "-h", "--help" -> TODO("Help is not implemented yet")
+
+ "--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) {
+ throw ArgumentsException("Duplicate or conflicting argument found: $arg")
+ }
+ }
+
+ if (!ret.inJar.isSet) {
+ throw ArgumentsException("Required option missing: --in-jar")
+ }
+ if (!ret.outJar.isSet) {
+ throw ArgumentsException("Required option missing: --out-jar")
+ }
+ return ret
+ }
+ }
+
+ override fun toString(): String {
+ return """
+ 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
new file mode 100644
index 0000000..1aa70c08
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
@@ -0,0 +1,102 @@
+/*
+ * 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 android.platform.test.annotations.NoRavenizer
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.findAnyAnnotation
+import com.android.hoststubgen.asm.startsWithAny
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.objectweb.asm.Type
+
+data class TypeHolder(
+ val clazz: Class<*>,
+) {
+ val type = Type.getType(clazz)
+ val desc = type.descriptor
+ val descAsSet = setOf<String>(desc)
+ val internlName = type.internalName
+ val humanReadableName = type.internalName.toHumanReadableClassName()
+}
+
+val testAnotType = TypeHolder(org.junit.Test::class.java)
+val ruleAnotType = TypeHolder(org.junit.Rule::class.java)
+val classRuleAnotType = TypeHolder(org.junit.ClassRule::class.java)
+val runWithAnotType = TypeHolder(RunWith::class.java)
+val innerRunnerAnotType = TypeHolder(RavenwoodAwareTestRunner.InnerRunner::class.java)
+val noRavenizerAnotType = TypeHolder(NoRavenizer::class.java)
+
+val testRuleType = TypeHolder(TestRule::class.java)
+val ravenwoodTestRunnerType = TypeHolder(RavenwoodAwareTestRunner::class.java)
+
+/**
+ * Returns true, if a test looks like it's a test class which needs to be processed.
+ */
+fun isTestLookingClass(classes: ClassNodes, className: String): Boolean {
+ // Similar to com.android.tradefed.lite.HostUtils.testLoadClass(), except it's more lenient,
+ // and accept non-public and/or abstract classes.
+ // HostUtils also checks "Suppress" or "SuiteClasses" but this one doesn't.
+ // TODO: SuiteClasses may need to be supported.
+
+ val cn = classes.findClass(className) ?: return false
+
+ if (cn.findAnyAnnotation(runWithAnotType.descAsSet) != null) {
+ return true
+ }
+ cn.methods?.forEach { method ->
+ if (method.findAnyAnnotation(testAnotType.descAsSet) != null) {
+ return true
+ }
+ }
+
+ // Check the super class.
+ if (cn.superName == null) {
+ return false
+ }
+ return isTestLookingClass(classes, cn.superName)
+}
+
+fun String.isRavenwoodClass(): Boolean {
+ return this.startsWithAny(
+ "com/android/hoststubgen/",
+ "android/platform/test/ravenwood",
+ "com/android/ravenwood/",
+ "com/android/platform/test/ravenwood/",
+ )
+}
+
+/**
+ * Classes that should never be modified.
+ */
+fun String.shouldByBypassed(): Boolean {
+ if (this.isRavenwoodClass()) {
+ return true
+ }
+ 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
new file mode 100644
index 0000000..cf6d6f6
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt
@@ -0,0 +1,466 @@
+/*
+ * 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.adapter
+
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner
+import com.android.hoststubgen.ClassParseException
+import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC
+import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
+import com.android.hoststubgen.asm.CTOR_NAME
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.findAnnotationValueAsType
+import com.android.hoststubgen.asm.findAnyAnnotation
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import com.android.hoststubgen.log
+import com.android.hoststubgen.visitors.OPCODE_VERSION
+import com.android.platform.test.ravenwood.ravenizer.RavenizerInternalException
+import com.android.platform.test.ravenwood.ravenizer.classRuleAnotType
+import com.android.platform.test.ravenwood.ravenizer.isTestLookingClass
+import com.android.platform.test.ravenwood.ravenizer.innerRunnerAnotType
+import com.android.platform.test.ravenwood.ravenizer.noRavenizerAnotType
+import com.android.platform.test.ravenwood.ravenizer.ravenwoodTestRunnerType
+import com.android.platform.test.ravenwood.ravenizer.ruleAnotType
+import com.android.platform.test.ravenwood.ravenizer.runWithAnotType
+import com.android.platform.test.ravenwood.ravenizer.testRuleType
+import org.objectweb.asm.AnnotationVisitor
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.FieldVisitor
+import org.objectweb.asm.MethodVisitor
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.Opcodes.ACC_FINAL
+import org.objectweb.asm.Opcodes.ACC_PUBLIC
+import org.objectweb.asm.Opcodes.ACC_STATIC
+import org.objectweb.asm.commons.AdviceAdapter
+import org.objectweb.asm.tree.ClassNode
+
+/**
+ * Class visitor to update the RunWith and inject some necessary rules.
+ *
+ * - Change the @RunWith(RavenwoodAwareTestRunner.class).
+ * - If the original class has a @RunWith(...), then change it to an @OrigRunWith(...).
+ * - Add RavenwoodAwareTestRunner's member rules as junit rules.
+ * - Update the order of the existing JUnit rules to make sure they don't use the MIN or MAX.
+ */
+class RunnerRewritingAdapter private constructor(
+ protected val classes: ClassNodes,
+ nextVisitor: ClassVisitor,
+) : ClassVisitor(OPCODE_VERSION, nextVisitor) {
+ /** Arbitrary cut-off point when deciding whether to change the order or an existing rule.*/
+ val RULE_ORDER_TWEAK_CUTOFF = 1973020500
+
+ /** Current class's internal name */
+ lateinit var classInternalName: String
+
+ /** [ClassNode] for the current class */
+ lateinit var classNode: ClassNode
+
+ /** True if this visitor is generating code. */
+ var isGeneratingCode = false
+
+ /** Run a [block] with [isGeneratingCode] set to true. */
+ private inline fun <T> generateCode(block: () -> T): T {
+ isGeneratingCode = true
+ try {
+ return block()
+ } finally {
+ isGeneratingCode = false
+ }
+ }
+
+ override fun visit(
+ version: Int,
+ access: Int,
+ name: String?,
+ signature: String?,
+ superName: String?,
+ interfaces: Array<out String>?,
+ ) {
+ classInternalName = name!!
+ classNode = classes.getClass(name)
+ if (!isTestLookingClass(classes, name)) {
+ throw RavenizerInternalException("This adapter shouldn't be used for non-test class")
+ }
+ super.visit(version, access, name, signature, superName, interfaces)
+
+ generateCode {
+ injectRunWithAnnotation()
+ if (!classes.hasClassInitializer(classInternalName)) {
+ injectStaticInitializer()
+ }
+ injectRules()
+ }
+ }
+
+ /**
+ * Remove the original @RunWith annotation.
+ */
+ override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {
+ if (!isGeneratingCode && runWithAnotType.desc == descriptor) {
+ return null
+ }
+ return super.visitAnnotation(descriptor, visible)
+ }
+
+ override fun visitField(
+ access: Int,
+ name: String,
+ descriptor: String,
+ signature: String?,
+ value: Any?
+ ): FieldVisitor {
+ val fallback = super.visitField(access, name, descriptor, signature, value)
+ if (isGeneratingCode) {
+ return fallback
+ }
+ return FieldRuleOrderRewriter(name, fallback)
+ }
+
+ /** Inject an empty <clinit>. The body will be injected by [visitMethod]. */
+ private fun injectStaticInitializer() {
+ visitMethod(
+ Opcodes.ACC_PRIVATE or Opcodes.ACC_STATIC,
+ CLASS_INITIALIZER_NAME,
+ CLASS_INITIALIZER_DESC,
+ null,
+ null
+ )!!.let { mv ->
+ mv.visitCode()
+ mv.visitInsn(Opcodes.RETURN)
+ mv.visitMaxs(0, 0)
+ mv.visitEnd()
+ }
+ }
+
+ /**
+ * Inject `@RunWith(RavenwoodAwareTestRunner.class)`. If the class already has
+ * a `@RunWith`, then change it to add a `@OrigRunWith`.
+ */
+ private fun injectRunWithAnnotation() {
+ // Extract the original RunWith annotation and its value.
+ val runWith = classNode.findAnyAnnotation(runWithAnotType.descAsSet)
+ val runWithClass = runWith?.let { an ->
+ findAnnotationValueAsType(an, "value")
+ }
+
+ if (runWith != null) {
+ if (runWithClass == ravenwoodTestRunnerType.type) {
+ // It already uses RavenwoodTestRunner. We'll just keep it, but we need to
+ // inject it again because the original one is removed by visitAnnotation().
+ log.d("Class ${classInternalName.toHumanReadableClassName()}" +
+ " already uses RavenwoodTestRunner.")
+ visitAnnotation(runWithAnotType.desc, true)!!.let { av ->
+ av.visit("value", ravenwoodTestRunnerType)
+ av.visitEnd()
+ }
+ return
+ }
+ if (runWithClass == null) {
+ throw ClassParseException("@RunWith annotation doesn't have a property \"value\""
+ + " in class ${classInternalName.toHumanReadableClassName()}")
+ }
+
+ // Inject an @OrigRunWith.
+ visitAnnotation(innerRunnerAnotType.desc, true)!!.let { av ->
+ av.visit("value", runWithClass)
+ av.visitEnd()
+ }
+ }
+
+ // Inject a @RunWith(RavenwoodAwareTestRunner.class).
+ visitAnnotation(runWithAnotType.desc, true)!!.let { av ->
+ av.visit("value", ravenwoodTestRunnerType.type)
+ av.visitEnd()
+ }
+ log.v("Update the @RunWith: ${classInternalName.toHumanReadableClassName()}")
+ }
+
+ /*
+ Generate the fields and the ctor, which should looks like this:
+
+ public static final org.junit.rules.TestRule sRavenwoodImplicitClassMinRule;
+ descriptor: Lorg/junit/rules/TestRule;
+ flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
+ RuntimeVisibleAnnotations:
+ 0: #49(#50=I#51)
+ org.junit.ClassRule(
+ order=-2147483648
+ )
+
+ public static final org.junit.rules.TestRule sRavenwoodImplicitClassMaxRule;
+ descriptor: Lorg/junit/rules/TestRule;
+ flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
+ RuntimeVisibleAnnotations:
+ 0: #49(#50=I#52)
+ org.junit.ClassRule(
+ order=2147483647
+ )
+
+ public final org.junit.rules.TestRule sRavenwoodImplicitInstanceMinRule;
+ descriptor: Lorg/junit/rules/TestRule;
+ flags: (0x0011) ACC_PUBLIC, ACC_FINAL
+ RuntimeVisibleAnnotations:
+ 0: #53(#50=I#51)
+ org.junit.Rule(
+ order=-2147483648
+ )
+
+ public final org.junit.rules.TestRule sRavenwoodImplicitInstanceMaxRule;
+ descriptor: Lorg/junit/rules/TestRule;
+ flags: (0x0011) ACC_PUBLIC, ACC_FINAL
+ RuntimeVisibleAnnotations:
+ 0: #53(#50=I#52)
+ org.junit.Rule(
+ order=2147483647
+ )
+ */
+
+ val sRavenwood_ClassRuleMin = "sRavenwood_ClassRuleMin"
+ val sRavenwood_ClassRuleMax = "sRavenwood_ClassRuleMax"
+ val mRavenwood_InstRuleMin = "mRavenwood_InstRuleMin"
+ val mRavenwood_InstRuleMax = "mRavenwood_InstRuleMax"
+
+ private fun injectRules() {
+ injectRule(sRavenwood_ClassRuleMin, true, Integer.MIN_VALUE)
+ injectRule(sRavenwood_ClassRuleMax, true, Integer.MAX_VALUE)
+ injectRule(mRavenwood_InstRuleMin, false, Integer.MIN_VALUE)
+ injectRule(mRavenwood_InstRuleMax, false, Integer.MAX_VALUE)
+ }
+
+ private fun injectRule(fieldName: String, isStatic: Boolean, order: Int) {
+ visitField(
+ ACC_PUBLIC or ACC_FINAL or (if (isStatic) ACC_STATIC else 0),
+ fieldName,
+ testRuleType.desc,
+ null,
+ null,
+ ).let { fv ->
+ val anot = if (isStatic) { classRuleAnotType } else { ruleAnotType }
+ fv.visitAnnotation(anot.desc, true).let {
+ it.visit("order", order)
+ it.visitEnd()
+ }
+ fv.visitEnd()
+ }
+ }
+
+ override fun visitMethod(
+ access: Int,
+ name: String,
+ descriptor: String,
+ signature: String?,
+ exceptions: Array<String>?,
+ ): MethodVisitor {
+ val next = super.visitMethod(access, name, descriptor, signature, exceptions)
+ if (name == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC) {
+ return ClassInitializerVisitor(
+ access, name, descriptor, signature, exceptions, next)
+ }
+ if (name == CTOR_NAME) {
+ return ConstructorVisitor(
+ access, name, descriptor, signature, exceptions, next)
+ }
+ return next
+ }
+
+ /*
+
+ static {};
+ descriptor: ()V
+ flags: (0x0008) ACC_STATIC
+ Code:
+ stack=1, locals=0, args_size=0
+ 0: getstatic #36 // Field android/platform/test/ravenwood/RavenwoodAwareTestRunner.RavenwoodImplicitClassMinRule:Lorg/junit/rules/TestRule;
+ 3: putstatic #39 // Field sRavenwoodImplicitClassMinRule:Lorg/junit/rules/TestRule;
+ 6: getstatic #42 // Field android/platform/test/ravenwood/RavenwoodAwareTestRunner.RavenwoodImplicitClassMaxRule:Lorg/junit/rules/TestRule;
+ 9: putstatic #45 // Field sRavenwoodImplicitClassMaxRule:Lorg/junit/rules/TestRule;
+ 12: return
+ LineNumberTable:
+ line 33: 0
+ line 36: 6
+ */
+ private inner class ClassInitializerVisitor(
+ access: Int,
+ val name: String,
+ val descriptor: String,
+ signature: String?,
+ exceptions: Array<String>?,
+ next: MethodVisitor?,
+ ) : MethodVisitor(OPCODE_VERSION, next) {
+ override fun visitCode() {
+ visitFieldInsn(Opcodes.GETSTATIC,
+ ravenwoodTestRunnerType.internlName,
+ RavenwoodAwareTestRunner.IMPLICIT_CLASS_OUTER_RULE_NAME,
+ testRuleType.desc
+ )
+ visitFieldInsn(Opcodes.PUTSTATIC,
+ classInternalName,
+ sRavenwood_ClassRuleMin,
+ testRuleType.desc
+ )
+
+ visitFieldInsn(Opcodes.GETSTATIC,
+ ravenwoodTestRunnerType.internlName,
+ RavenwoodAwareTestRunner.IMPLICIT_CLASS_INNER_RULE_NAME,
+ testRuleType.desc
+ )
+ visitFieldInsn(Opcodes.PUTSTATIC,
+ classInternalName,
+ sRavenwood_ClassRuleMax,
+ testRuleType.desc
+ )
+
+ super.visitCode()
+ }
+ }
+
+ /*
+ public com.android.ravenwoodtest.bivalenttest.runnertest.RavenwoodRunnerTest();
+ descriptor: ()V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=2, locals=1, args_size=1
+ 0: aload_0
+ 1: invokespecial #1 // Method java/lang/Object."<init>":()V
+ 4: aload_0
+ 5: getstatic #7 // Field android/platform/test/ravenwood/RavenwoodAwareTestRunner.RavenwoodImplicitInstanceMinRule:Lorg/junit/rules/TestRule;
+ 8: putfield #13 // Field sRavenwoodImplicitInstanceMinRule:Lorg/junit/rules/TestRule;
+ 11: aload_0
+ 12: getstatic #18 // Field android/platform/test/ravenwood/RavenwoodAwareTestRunner.RavenwoodImplicitInstanceMaxRule:Lorg/junit/rules/TestRule;
+ 15: putfield #21 // Field sRavenwoodImplicitInstanceMaxRule:Lorg/junit/rules/TestRule;
+ 18: return
+ LineNumberTable:
+ line 31: 0
+ line 38: 4
+ line 41: 11
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 19 0 this Lcom/android/ravenwoodtest/bivalenttest/runnertest/RavenwoodRunnerTest;
+ */
+ private inner class ConstructorVisitor(
+ access: Int,
+ name: String,
+ descriptor: String,
+ signature: String?,
+ exceptions: Array<String>?,
+ next: MethodVisitor?,
+ ) : AdviceAdapter(OPCODE_VERSION, next, ACC_ENUM, name, descriptor) {
+ override fun onMethodEnter() {
+ visitVarInsn(ALOAD, 0)
+ visitFieldInsn(Opcodes.GETSTATIC,
+ ravenwoodTestRunnerType.internlName,
+ RavenwoodAwareTestRunner.IMPLICIT_INST_OUTER_RULE_NAME,
+ testRuleType.desc
+ )
+ visitFieldInsn(Opcodes.PUTFIELD,
+ classInternalName,
+ mRavenwood_InstRuleMin,
+ testRuleType.desc
+ )
+
+ visitVarInsn(ALOAD, 0)
+ visitFieldInsn(Opcodes.GETSTATIC,
+ ravenwoodTestRunnerType.internlName,
+ RavenwoodAwareTestRunner.IMPLICIT_INST_INNER_RULE_NAME,
+ testRuleType.desc
+ )
+ visitFieldInsn(Opcodes.PUTFIELD,
+ classInternalName,
+ mRavenwood_InstRuleMax,
+ testRuleType.desc
+ )
+ }
+ }
+
+ /**
+ * Rewrite "order" of the existing junit rules to make sure no rules use a MAX or MIN order.
+ *
+ * Currently, we do it a hacky way -- use an arbitrary cut-off point, and if the order
+ * is larger than that, decrement by 1, and if it's smaller than the negative cut-off point,
+ * increment it by 1.
+ *
+ * (or the arbitrary number is already used.... then we're unlucky, let's change the cut-off
+ * point.)
+ */
+ private inner class FieldRuleOrderRewriter(
+ val fieldName: String,
+ next: FieldVisitor,
+ ) : FieldVisitor(OPCODE_VERSION, next) {
+ override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
+ val fallback = super.visitAnnotation(descriptor, visible)
+ if (descriptor != ruleAnotType.desc && descriptor != classRuleAnotType.desc) {
+ return fallback
+ }
+ return RuleOrderRewriter(fallback)
+ }
+
+ private inner class RuleOrderRewriter(
+ next: AnnotationVisitor,
+ ) : AnnotationVisitor(OPCODE_VERSION, next) {
+ override fun visit(name: String?, origValue: Any?) {
+ if (name != "order") {
+ return super.visit(name, origValue)
+ }
+ var order = origValue as Int
+ if (order == RULE_ORDER_TWEAK_CUTOFF || order == -RULE_ORDER_TWEAK_CUTOFF) {
+ // Oops. If this happens, we'll need to change RULE_ORDER_TWEAK_CUTOFF.
+ // Or, we could scan all the rules in the target jar and find an unused number.
+ // Because rules propagate to subclasses, we'll at least check all the
+ // super classes of the current class.
+ throw RavenizerInternalException(
+ "OOPS: Field $classInternalName.$fieldName uses $order."
+ + " We can't update it.")
+ }
+ if (order > RULE_ORDER_TWEAK_CUTOFF) {
+ order -= 1
+ }
+ if (order < -RULE_ORDER_TWEAK_CUTOFF) {
+ order += 1
+ }
+ super.visit(name, order)
+ }
+ }
+ }
+
+ companion object {
+ fun shouldProcess(classes: ClassNodes, className: String): Boolean {
+ if (!isTestLookingClass(classes, className)) {
+ return false
+ }
+ // Don't process a class if it has a @NoRavenizer annotation.
+ classes.findClass(className)?.let { cn ->
+ if (cn.findAnyAnnotation(noRavenizerAnotType.descAsSet) != null) {
+ log.i("Class ${className.toHumanReadableClassName()} has" +
+ " @${noRavenizerAnotType.humanReadableName}. Skipping."
+ )
+ return false
+ }
+ }
+ return true
+ }
+
+ fun maybeApply(
+ className: String,
+ classes: ClassNodes,
+ nextVisitor: ClassVisitor,
+ ): ClassVisitor {
+ if (!shouldProcess(classes, className)) {
+ return nextVisitor
+ } else {
+ return RunnerRewritingAdapter(classes, nextVisitor)
+ }
+ }
+ }
+}
\ No newline at end of file