diff options
author | 2024-09-26 22:59:40 +0000 | |
---|---|---|
committer | 2024-09-26 22:59:40 +0000 | |
commit | 983461633b96db0bc58205a657edeffad3ce4080 (patch) | |
tree | 8df76979756f92a675ea35ac852917a2369d152f /ravenwood | |
parent | 6f7665370b368b3f4164cdce501ae224b3351c9b (diff) |
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
Diffstat (limited to 'ravenwood')
109 files changed, 8358 insertions, 1857 deletions
diff --git a/ravenwood/.gitignore b/ravenwood/.gitignore new file mode 100644 index 000000000000..751553b3acb9 --- /dev/null +++ b/ravenwood/.gitignore @@ -0,0 +1 @@ +*.bak diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index ac62687f1b77..10e4f3820cd7 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -5,6 +5,10 @@ package { // 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 @@ java_library { libs: [ "ravenwood-runtime-common-ravenwood", ], + static_libs: [ + "framework-annotations-lib", // should it be "libs" instead? + ], visibility: ["//visibility:private"], } @@ -126,8 +133,9 @@ java_library { ], libs: [ "framework-minus-apex.ravenwood", - "ravenwood-junit", + "ravenwood-helper-libcore-runtime", ], + sdk_version: "core_current", visibility: ["//visibility:private"], } @@ -160,11 +168,22 @@ java_library { "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 @@ java_library { libs: [ "junit", "flag-junit", + "framework-annotations-lib", ], visibility: ["//visibility:public"], } @@ -279,10 +299,10 @@ sh_test_host { 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 57548378579c..86246e2dcc2a 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,12 +21,74 @@ } ], "ravenwood-presubmit": [ + // AUTO-GENERATED-START + // DO NOT MODIFY MANUALLY + // Use scripts/update-test-mapping.sh to update it. { - "name": "RavenwoodMinimumTest", + "name": "AdServicesSharedLibrariesUnitTestsRavenwood", "host": true }, { - "name": "RavenwoodMockitoTest", + "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 }, { @@ -39,12 +96,79 @@ "host": true }, { - "name": "RavenwoodCoreTest", + "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 + }, + { + "name": "RavenwoodMockitoTest", + "host": true + }, + { + "name": "RavenwoodResApkTest", + "host": true + }, + { + "name": "RavenwoodRuntimeTest", + "host": true + }, + { + "name": "RavenwoodServicesTest", + "host": true + } + // AUTO-GENERATED-END + ], + "ravenwood-postsubmit": [ + { + "name": "SystemUiRavenTests", + "host": true } ] } diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java new file mode 100644 index 000000000000..b582ccf7b656 --- /dev/null +++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java @@ -0,0 +1,35 @@ +/* + * 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.ravenwood.annotation; + +import static java.lang.annotation.ElementType.METHOD; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * THIS ANNOTATION IS EXPERIMENTAL. REACH OUT TO g/ravenwood BEFORE USING IT, OR YOU HAVE ANY + * QUESTIONS ABOUT IT. + * + * TODO: Javadoc + * + * @hide + */ +@Target({METHOD}) +@Retention(RetentionPolicy.CLASS) +public @interface RavenwoodRedirect { +} diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java index 4b9cf85e16fa..bee9222ae5eb 100644 --- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java +++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java @@ -31,6 +31,6 @@ import java.lang.annotation.Target; */ @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 06cf08e6c3df..e897735493a3 100644 --- a/ravenwood/bivalenttest/Android.bp +++ b/ravenwood/bivalenttest/Android.bp @@ -39,6 +39,9 @@ android_ravenwood_test { "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 @@ android_test { "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 3a24c0e829a4..e8f59db86901 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.ClassRule; 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/RavenwoodConfigTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java new file mode 100644 index 000000000000..a5a16c14600b --- /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 000000000000..c25d2b4cbc4d --- /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/RavenwoodClassRuleRavenwoodOnlyTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNoConfigNoRuleTest.java index aa33dc3392b0..d47330568828 100644 --- a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleRavenwoodOnlyTest.java +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNoConfigNoRuleTest.java @@ -15,29 +15,22 @@ */ package com.android.ravenwoodtest.bivalenttest; -import android.platform.test.ravenwood.RavenwoodClassRule; -import android.platform.test.ravenwood.RavenwoodRule; +import static org.junit.Assert.assertNotNull; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; -import org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Ignore; 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) -// 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(); +public class RavenwoodNoConfigNoRuleTest { @Test - public void testRavenwoodOnly() { - Assert.assertTrue(RavenwoodRule.isOnRavenwood()); + 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 01e90d8672e4..3de372e48e3a 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 @@ public class RavenwoodRuleTest { } @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 000000000000..09a0aa8dbaa2 --- /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 000000000000..d7c2c6cd73a8 --- /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 000000000000..7ef672e80bee --- /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 000000000000..7ef40dc49e1a --- /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/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java index d952d07b3817..ae596b10848b 100644 --- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java @@ -13,44 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.ravenwoodtest.coretest.methodvalidation; +package com.android.ravenwoodtest.bivalenttest.ravenizer; -import android.platform.test.ravenwood.RavenwoodRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; -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. + * 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 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() { - } - +public class RavenwoodImplicitRuleShadowingTest extends RavenwoodImplicitRuleShadowingTestBase { @Test - public void testEmpty() { + public void testOkInSubClass() { } } diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java index 5a3589dae43a..1ca97af632dd 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java +++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java @@ -13,19 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.ravenwoodtest.bivalenttest.ravenizer; -package com.android.platform.test.ravenwood.nativesubstitution; +import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.android.ravenwood.common.JvmWorkaround; +import org.junit.Test; +import org.junit.runner.RunWith; -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); +/** + * 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 000000000000..9d878f444e5e --- /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 000000000000..c77841b1b55a --- /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 000000000000..ea1a29d57482 --- /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 000000000000..c042eb010558 --- /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 000000000000..2feb5ba9aa01 --- /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 000000000000..7e3bc0fccd7f --- /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 000000000000..7e396c2080eb --- /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) +@Suite.SuiteClasses({ + 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 b60bfbfcb6f4..000000000000 --- 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 f1e33cb686f1..000000000000 --- 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 db95fad2a3ad..000000000000 --- 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 ddc66c73a7c0..000000000000 --- 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 ec8e907dcdb3..000000000000 --- 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/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 000000000000..478bead1354f --- /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 000000000000..3535cb2b1b79 --- /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 1dd5e1ddd630..239c8061b757 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.ravenwood.example.RedManager; 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 @@ public class RavenwoodContext extends RavenwoodBaseContext { 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 @@ public class RavenwoodContext extends RavenwoodBaseContext { } } + void cleanUp() { + deleteDir(mFilesDir); + deleteDir(mCacheDir); + } + @Override public String getSystemServiceName(Class<?> serviceClass) { // TODO: pivot to using SystemServiceRegistry @@ -100,34 +133,35 @@ public class RavenwoodContext extends RavenwoodBaseContext { @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 @@ public class RavenwoodContext extends RavenwoodBaseContext { 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 class RavenwoodContext extends RavenwoodBaseContext { 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 000000000000..77275c445dd9 --- /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 000000000000..e5486117e7f2 --- /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 4357f2b8660a..000000000000 --- 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 000000000000..03513ab0a2af --- /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 000000000000..40b14dbe6bfd --- /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 cd6b61df392f..3946dd8471b0 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 @@ public class RavenwoodSystemServer { 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 @@ public class RavenwoodSystemServer { 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 000000000000..428eb57f20bf --- /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/NoRavenizer.java index 2fb8074c850a..a84f16f619d3 100644 --- a/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java +++ b/ravenwood/junit-src/android/platform/test/annotations/NoRavenizer.java @@ -23,29 +23,14 @@ 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.) + * 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.METHOD}) +@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) -public @interface DisabledOnNonRavenwood { - /** - * General free-form description of why this test is being ignored. - */ - String reason() default ""; +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 000000000000..5ba972df1193 --- /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 f4b7ec360dbf..85297fe96d6a 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 000000000000..446f819ad41b --- /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 825c91a1b6dc..4196d8e22610 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 org.junit.Assert.fail; +import static com.android.ravenwood.common.RavenwoodCommonUtils.log; +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 @@ public class RavenwoodRule implements TestRule { * * 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 @@ public class RavenwoodRule implements TestRule { * test. Has no effect on non-Ravenwood environments. */ public Builder setProcessSystem() { - mRule.mUid = SYSTEM_UID; + mBuilder.setProcessSystem(); return this; } @@ -158,7 +123,7 @@ public class RavenwoodRule implements TestRule { * test. Has no effect on non-Ravenwood environments. */ public Builder setProcessApp() { - mRule.mUid = FIRST_APPLICATION_UID; + mBuilder.setProcessApp(); return this; } @@ -166,8 +131,8 @@ public class RavenwoodRule implements TestRule { * 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 @@ public class RavenwoodRule implements TestRule { * 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 @@ public class RavenwoodRule implements TestRule { * * 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 @@ public class RavenwoodRule implements TestRule { * * 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 @@ public class RavenwoodRule implements TestRule { * {@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,183 +210,49 @@ public class RavenwoodRule implements TestRule { } /** - * 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); - } - } - }; - } - - /** - * 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"); + RavenwoodAwareTestRunnerHook.onRavenwoodRuleExit( + RavenwoodAwareTestRunner.getCurrentRunner(), description, + RavenwoodRule.this); } } }; } - public static class _$RavenwoodPrivate { - public static boolean isOptionalValidationEnabled() { - return ENABLE_OPTIONAL_VALIDATION; - } - } - /** * Returns the "real" result from {@link System#currentTimeMillis()}. * @@ -439,4 +263,51 @@ public class RavenwoodRule implements TestRule { 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 5f1b0c2c929f..ef8f5840f949 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 @@ public class RavenwoodSystemProperties { 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 000000000000..aa8c29936082 --- /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 000000000000..43a28ba72ec9 --- /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 483b98a96034..000000000000 --- 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 000000000000..83cbc5265d8f --- /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 index b4771171815a..30abaa2e7d38 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 index 1029ed2bee3b..e547114bbe40 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 0238baa2dcbf..02153a757cd8 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 class JvmWorkaround { */ public abstract void setFdInt(FileDescriptor fd, int fdInt); - /** * Equivalent to Android's FileDescriptor.getInt$(). */ @@ -49,6 +48,10 @@ public abstract class JvmWorkaround { */ 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 abstract class JvmWorkaround { 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 a260147654cd..2323c65de5cf 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 @@ package com.android.ravenwood.common; 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 @@ class OpenJdkWorkaround extends JvmWorkaround { + " 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 c8cc8d9fe273..989bb6be1782 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 @@ public class RavenwoodCommonUtils { 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 @@ public class RavenwoodCommonUtils { 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 @@ public class RavenwoodCommonUtils { 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 class RavenwoodCommonUtils { */ public static String getRavenwoodRuntimePath() { ensureOnRavenwood(); - return RAVEWOOD_RUNTIME_PATH; + return RAVENWOOD_RUNTIME_PATH; } private static String getRavenwoodRuntimePathInternal() { @@ -231,4 +268,37 @@ public class RavenwoodCommonUtils { 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 index f38d5653d3a9..e21a9cd71a2d 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 @@ public class CursorWindow_host { 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 class CursorWindow_host { 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 @@ public class CursorWindow_host { 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 @@ public class CursorWindow_host { 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 @@ public class CursorWindow_host { 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 @@ public class CursorWindow_host { 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 @@ public class CursorWindow_host { 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 index 5e81124b6e70..1b63adc4319f 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 index e7479d313918..b09bf3119cfa 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 @@ public class SystemProperties_host { /** 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 @@ public class SystemProperties_host { 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 @@ public class SystemProperties_host { 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 @@ public class SystemProperties_host { } else { sValues.put(key, val); } - sChangeCallback.run(); + SystemProperties.callChangeCallbacks(); } } @@ -183,7 +182,7 @@ public class SystemProperties_host { // 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 index 55d4ffb41e78..878a0ff57a1d 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 @@ public class EventLog_host { 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 index f301b9c46b0e..d232ef2076be 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 @@ public class Log_host { 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 @@ public class Log_host { 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 000000000000..c18c307ad1e3 --- /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 index 0f65544f8b66..9ce8ea8e16ef 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 @@ import java.util.HashMap; 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 class LongArrayMultiStateCounter_host { } } - 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 index 9486651ce48d..1d95aa143549 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 000000000000..e12ff240c4d9 --- /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/Parcel_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java deleted file mode 100644 index 2df93cd93935..000000000000 --- 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 b00cee02f611..000000000000 --- 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 e198646d4e27..be8c44388435 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 class ClassLoadHook { 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"); - } - } - - 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()); + // 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 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 ecaa8161ee46..c94ef31a5e5e 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 @@ public final class Os { 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 final class Os { 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 000000000000..96aed4b3401d --- /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 index beba83391652..ad80d92686ab 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 @@ public class RavenwoodRuntimeNative { 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 7d2b00d9420d..ba89f71dde8a 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 @@ package dalvik.system; // 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 class VMRuntime { } 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 65c285e06bf8..2bd1ae89c824 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 final class IoUtils { } } + 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 @@ public final class IoUtils { } } } + + /** + * 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 000000000000..478503b699a0 --- /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 14b5a4f0c1e0..4e7dc5d6264f 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 c8049281bc53..c255be5f61aa 100644 --- a/ravenwood/runtime-jni/ravenwood_runtime.cpp +++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp @@ -214,6 +214,19 @@ static jint Linux_open(JNIEnv* env, jobject, jstring javaPath, jint flags, jint 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 @@ static const JNINativeMethod sMethods[] = { "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 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) 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 index 3332e24ea013..633ed4e9d10a 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 index 05275b29e48b..c2230c739ccf 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 fb9b823ee93b..05f3fdffdaa7 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 index 84b3c8ee365e..be15b711b980 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 000000000000..8589c1d433fc --- /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 000000000000..e478b50cc2b9 --- /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 044239f06297..8ce15f006ac2 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 @@ public class RavenwoodServicesTest { } @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 0a0b200adabc..c29fb7f67e78 100644 --- a/ravenwood/test-authors.md +++ b/ravenwood/test-authors.md @@ -46,7 +46,7 @@ android_ravenwood_test { * 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 @@ public class MyCodeTest { * 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 @@ public class MyCodeTest { } @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 000000000000..41e45e5a6d95 --- /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 000000000000..a5a1f17f5ec0 --- /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 000000000000..3dc4c566220c --- /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 000000000000..9491c5315e2a --- /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 000000000000..3079c0612c3c --- /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 000000000000..73ef650a9780 --- /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 index 7e2c407f2116..7528a6270ae1 100644 --- a/ravenwood/tools/ravenizer-fake/Android.bp +++ b/ravenwood/tests/bivalentinst/targetapp/Android.bp @@ -7,8 +7,14 @@ package { 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 000000000000..0715f5d62654 --- /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 000000000000..395bc2ae37e2 --- /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 000000000000..15e50ecd4e4a --- /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 000000000000..ed1992cdd1ea --- /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 000000000000..b5bafc435845 --- /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 index a78c5c1e8227..d94475c00240 100644 --- a/ravenwood/coretest/Android.bp +++ b/ravenwood/tests/coretest/Android.bp @@ -14,10 +14,12 @@ android_ravenwood_test { "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 000000000000..bd013133d3a4 --- /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 000000000000..73ea64f57997 --- /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 000000000000..9a6934bf17c5 --- /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 f3172ae3090d..9c8638930df9 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.FakeMetricsLogger 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.ContainerHelpers 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.SparseDoubleArray 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.BadTypeParcelableException 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.ConditionVariable 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.RemoteCallbackList 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.TimestampedValue 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.ApplicationInfo 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.ServiceInfo 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.CursorWrapper 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.SQLiteClosable android.database.sqlite.SQLiteException android.text.TextUtils -android.text.TextUtils$SimpleStringSplitter android.accounts.Account @@ -244,7 +252,11 @@ android.graphics.ColorSpace 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.content.ContentProvider 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.CellSignalStrength 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.ProcFileReader 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 4012bdc5be62..d962c8232bf7 100644 --- a/ravenwood/texts/ravenwood-framework-policies.txt +++ b/ravenwood/texts/ravenwood-framework-policies.txt @@ -9,11 +9,21 @@ class :feature_flags keepclass # 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 @@ class android.content.pm.PackageManager keep 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 f64f26d7962a..3ec3e3ce2946 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 000000000000..2892d0778ec6 --- /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 000000000000..0dcd271562d1 --- /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 000000000000..f7f9a8563656 --- /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 000000000000..ff41818cd370 --- /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 000000000000..10fe0a3b5a93 --- /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 000000000000..1aa70c08c254 --- /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 000000000000..27092d28ae5e --- /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 000000000000..cf6d6f6bcae3 --- /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 |