First pass of "real" services on Ravenwood.
One of our eventual goals with Ravenwood is to support usage of
system services from test code. Robolectric takes the approach of
publishing "shadows" which are effectively fakes of the Manager
objects visible to app processes, and it unfortunately doesn't offer
a mechanism to run "real" services code.
In contrast, Ravenwood aims to support API owners progressively
offering their system services either via a "fake" approach, or by
using various levels of the "real" code that would run on a device.
This change wires up the foundational support and uses the simple
`SerialManager` example to demonstrate using the same "real" code
on both Ravenwood and devices. It also demonstrates the `Internal`
pattern being used to customize behavior for tests.
To offer as hermetic as a test environment as possible, we start
new instances of each requested service for each test. Requiring
developers to be explicit about the services they need will help
keep overhead low, especially for tests that don't need services.
Bug: 325506297
Test: atest RavenwoodServicesTest
Change-Id: Ie22436b38f2176f91dfce746b899ebab7752bbb8
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 132804f..53897e1 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -74,11 +74,14 @@
"androidx.test.monitor-for-device",
],
libs: [
+ "android.test.mock",
"framework-minus-apex.ravenwood",
+ "services.core.ravenwood",
"junit",
],
sdk_version: "core_current",
visibility: ["//frameworks/base"],
+ jarjar_rules: ":ravenwood-services-jarjar-rules",
}
// Carefully compiles against only test_current to support tests that
@@ -111,3 +114,9 @@
"androidx.test.monitor",
],
}
+
+filegroup {
+ name: "ravenwood-services-jarjar-rules",
+ srcs: ["ravenwood-services-jarjar-rules.txt"],
+ visibility: ["//frameworks/base"],
+}
diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodReplace.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodReplace.java
index a920f63..83a7b6e 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodReplace.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodReplace.java
@@ -32,4 +32,14 @@
@Target({METHOD})
@Retention(RetentionPolicy.CLASS)
public @interface RavenwoodReplace {
+ /**
+ * One or more classes that aren't yet supported by Ravenwood, which is why this method is
+ * being replaced.
+ */
+ Class<?>[] blockedBy() default {};
+
+ /**
+ * General free-form description of why this method is being replaced.
+ */
+ String reason() default "";
}
diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt
index 49cef07..6b67364 100644
--- a/ravenwood/framework-minus-apex-ravenwood-policies.txt
+++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt
@@ -52,5 +52,6 @@
method <init> ()V stub
class android.content.Context stub
method <init> ()V stub
+ method getSystemService (Ljava/lang/Class;)Ljava/lang/Object; stub
class android.content.pm.PackageManager stub
method <init> ()V stub
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
new file mode 100644
index 0000000..3668b03
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
@@ -0,0 +1,90 @@
+/*
+ * 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.content.Context;
+import android.hardware.ISerialManager;
+import android.hardware.SerialManager;
+import android.os.PermissionEnforcer;
+import android.os.ServiceManager;
+import android.test.mock.MockContext;
+import android.util.ArrayMap;
+import android.util.Singleton;
+
+import java.util.function.Supplier;
+
+public class RavenwoodContext extends MockContext {
+ private final RavenwoodPermissionEnforcer mEnforcer = new RavenwoodPermissionEnforcer();
+
+ private final ArrayMap<Class<?>, String> mClassToName = new ArrayMap<>();
+ private final ArrayMap<String, Supplier<?>> mNameToFactory = new ArrayMap<>();
+
+ private void registerService(Class<?> serviceClass, String serviceName,
+ Supplier<?> serviceSupplier) {
+ mClassToName.put(serviceClass, serviceName);
+ mNameToFactory.put(serviceName, serviceSupplier);
+ }
+
+ public RavenwoodContext() {
+ registerService(PermissionEnforcer.class,
+ Context.PERMISSION_ENFORCER_SERVICE, () -> mEnforcer);
+ registerService(SerialManager.class,
+ Context.SERIAL_SERVICE, asSingleton(() ->
+ new SerialManager(this, ISerialManager.Stub.asInterface(
+ ServiceManager.getService(Context.SERIAL_SERVICE)))
+ ));
+ }
+
+ @Override
+ public Object getSystemService(String serviceName) {
+ // TODO: pivot to using SystemServiceRegistry
+ final Supplier<?> serviceSupplier = mNameToFactory.get(serviceName);
+ if (serviceSupplier != null) {
+ return serviceSupplier.get();
+ } else {
+ throw new UnsupportedOperationException(
+ "Service " + serviceName + " not yet supported under Ravenwood");
+ }
+ }
+
+ @Override
+ public String getSystemServiceName(Class<?> serviceClass) {
+ // TODO: pivot to using SystemServiceRegistry
+ final String serviceName = mClassToName.get(serviceClass);
+ if (serviceName != null) {
+ return serviceName;
+ } else {
+ throw new UnsupportedOperationException(
+ "Service " + serviceClass + " not yet supported under Ravenwood");
+ }
+ }
+
+ /**
+ * Wrap the given {@link Supplier} to become a memoized singleton.
+ */
+ private static <T> Supplier<T> asSingleton(Supplier<T> supplier) {
+ final Singleton<T> singleton = new Singleton<>() {
+ @Override
+ protected T create() {
+ return supplier.get();
+ }
+ };
+ return () -> {
+ return singleton.get();
+ };
+ }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodPermissionEnforcer.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodPermissionEnforcer.java
new file mode 100644
index 0000000..4244135
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodPermissionEnforcer.java
@@ -0,0 +1,38 @@
+/*
+ * 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.permission.PermissionManager.PERMISSION_GRANTED;
+
+import android.content.AttributionSource;
+import android.os.PermissionEnforcer;
+
+public class RavenwoodPermissionEnforcer extends PermissionEnforcer {
+ @Override
+ protected int checkPermission(String permission, AttributionSource source) {
+ // For the moment, since Ravenwood doesn't offer cross-process capabilities, assume all
+ // permissions are granted during tests
+ return PERMISSION_GRANTED;
+ }
+
+ @Override
+ protected int checkPermission(String permission, int pid, int uid) {
+ // For the moment, since Ravenwood doesn't offer cross-process capabilities, assume all
+ // permissions are granted during tests
+ return PERMISSION_GRANTED;
+ }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index 1d5c79c..231cce9 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -24,11 +24,13 @@
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.Assert;
@@ -103,9 +105,10 @@
rule.mSystemProperties.getKeyReadablePredicate(),
rule.mSystemProperties.getKeyWritablePredicate());
- ActivityManager.init$ravenwood(rule.mCurrentUser);
+ ServiceManager.init$ravenwood();
+ LocalServices.removeAllServicesForTest();
- com.android.server.LocalServices.removeAllServicesForTest();
+ ActivityManager.init$ravenwood(rule.mCurrentUser);
if (rule.mProvideMainThread) {
final HandlerThread main = new HandlerThread(MAIN_THREAD_NAME);
@@ -113,7 +116,12 @@
Looper.setMainLooperForTest(main.getLooper());
}
- InstrumentationRegistry.registerInstance(new Instrumentation(), Bundle.EMPTY);
+ rule.mContext = new RavenwoodContext();
+ 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,
@@ -121,7 +129,7 @@
}
// Touch some references early to ensure they're <clinit>'ed
- Objects.requireNonNull(Build.IS_USERDEBUG);
+ Objects.requireNonNull(Build.TYPE);
Objects.requireNonNull(Build.VERSION.SDK);
}
@@ -130,17 +138,22 @@
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();
}
- com.android.server.LocalServices.removeAllServicesForTest();
-
ActivityManager.reset$ravenwood();
+ LocalServices.removeAllServicesForTest();
+ ServiceManager.reset$ravenwood();
+
android.os.SystemProperties.reset$ravenwood();
android.os.Binder.reset$ravenwood();
android.os.Process.reset$ravenwood();
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
new file mode 100644
index 0000000..bb280f4
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
@@ -0,0 +1,85 @@
+/*
+ * 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.hardware.SerialManager;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.SystemServiceManager;
+import com.android.server.utils.TimingsTraceAndSlog;
+
+public class RavenwoodSystemServer {
+ /**
+ * Set of services that we know how to provide under Ravenwood. We keep this set distinct
+ * from {@code com.android.server.SystemServer} to give us the ability to choose either
+ * "real" or "fake" implementations based on the commitments of the service owner.
+ *
+ * Map from {@code FooManager.class} to the {@code com.android.server.SystemService}
+ * lifecycle class name used to instantiate and drive that service.
+ */
+ private static final ArrayMap<Class<?>, String> sKnownServices = new ArrayMap<>();
+
+ // TODO: expand SystemService API to support dependency expression, so we don't need test
+ // authors to exhaustively declare all transitive services
+
+ static {
+ sKnownServices.put(SerialManager.class, "com.android.server.SerialService$Lifecycle");
+ }
+
+ private static TimingsTraceAndSlog sTimings;
+ private static SystemServiceManager sServiceManager;
+
+ public static void init(RavenwoodRule rule) {
+ // Avoid overhead if no services required
+ if (rule.mServicesRequired.isEmpty()) return;
+
+ sTimings = new TimingsTraceAndSlog();
+ sServiceManager = new SystemServiceManager(rule.mContext);
+ sServiceManager.setStartInfo(false,
+ SystemClock.elapsedRealtime(),
+ SystemClock.uptimeMillis());
+ LocalServices.addService(SystemServiceManager.class, sServiceManager);
+
+ for (Class<?> service : rule.mServicesRequired) {
+ final String target = sKnownServices.get(service);
+ if (target == null) {
+ throw new RuntimeException("The requested service " + service
+ + " is not yet supported under the Ravenwood deviceless testing "
+ + "environment; consider requesting support from the API owner or "
+ + "consider using Mockito; more details at go/ravenwood-docs");
+ } else {
+ sServiceManager.startService(target);
+ }
+ }
+ sServiceManager.sealStartedServices();
+
+ // TODO: expand to include additional boot phases when relevant
+ sServiceManager.startBootPhase(sTimings, SystemService.PHASE_SYSTEM_SERVICES_READY);
+ sServiceManager.startBootPhase(sTimings, SystemService.PHASE_BOOT_COMPLETED);
+ }
+
+ public static void reset(RavenwoodRule rule) {
+ // TODO: consider introducing shutdown boot phases
+
+ LocalServices.removeServiceForTest(SystemServiceManager.class);
+ sServiceManager = null;
+ sTimings = null;
+ }
+}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index b90f112..a8c24fc 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -22,10 +22,13 @@
import static org.junit.Assert.fail;
+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 android.util.ArraySet;
import org.junit.Assume;
import org.junit.rules.TestRule;
@@ -122,6 +125,11 @@
final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties();
+ final ArraySet<Class<?>> mServicesRequired = new ArraySet<>();
+
+ volatile Context mContext;
+ volatile Instrumentation mInstrumentation;
+
public RavenwoodRule() {
}
@@ -192,6 +200,23 @@
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(Class<?>... services) {
+ mRule.mServicesRequired.clear();
+ for (Class<?> service : services) {
+ mRule.mServicesRequired.add(service);
+ }
+ return this;
+ }
+
public RavenwoodRule build() {
return mRule;
}
@@ -212,6 +237,28 @@
return IS_ON_RAVENWOOD;
}
+ /**
+ * 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.
+ */
+ public Context getContext() {
+ return Objects.requireNonNull(mContext,
+ "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.
+ */
+ public Instrumentation getInstrumentation() {
+ return Objects.requireNonNull(mInstrumentation,
+ "Instrumentation is only available during @Test execution");
+ }
+
static boolean shouldEnableOnDevice(Description description) {
if (description.isTest()) {
if (description.getAnnotation(DisabledOnNonRavenwood.class) != null) {
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index b5baef6..4a4c290 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -99,6 +99,7 @@
android.util.StringBuilderPrinter
android.util.TeeWriter
android.util.TimeUtils
+android.util.TimingsTraceLog
android.util.UtilConfig
android.util.Xml
@@ -152,6 +153,7 @@
android.os.ParcelUuid
android.os.Parcelable
android.os.PatternMatcher
+android.os.PermissionEnforcer
android.os.PersistableBundle
android.os.PowerComponents
android.os.Process
@@ -159,6 +161,8 @@
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
@@ -252,6 +256,9 @@
android.view.Display$Mode
android.view.DisplayInfo
+android.hardware.SerialManager
+android.hardware.SerialManagerInternal
+
android.telephony.ActivityStatsTechSpecificInfo
android.telephony.CellSignalStrength
android.telephony.ModemActivityInfo
@@ -310,3 +317,9 @@
com.google.android.collect.Lists
com.google.android.collect.Maps
com.google.android.collect.Sets
+
+com.android.server.SerialService
+com.android.server.SystemService
+com.android.server.SystemServiceManager
+
+com.android.server.utils.TimingsTraceAndSlog
diff --git a/ravenwood/ravenwood-services-jarjar-rules.txt b/ravenwood/ravenwood-services-jarjar-rules.txt
new file mode 100644
index 0000000..8fdd340
--- /dev/null
+++ b/ravenwood/ravenwood-services-jarjar-rules.txt
@@ -0,0 +1,11 @@
+# Ignore one-off class defined out in core/java/
+rule com.android.server.LocalServices @0
+rule com.android.server.pm.pkg.AndroidPackage @0
+rule com.android.server.pm.pkg.AndroidPackageSplit @0
+
+# Rename all other service internals so that tests can continue to statically
+# link services code when owners aren't ready to support on Ravenwood
+rule com.android.server.** repackaged.@0
+
+# TODO: support AIDL generated Parcelables via hoststubgen
+rule android.hardware.power.stats.** repackaged.@0
diff --git a/ravenwood/services-test/Android.bp b/ravenwood/services-test/Android.bp
new file mode 100644
index 0000000..39858f0
--- /dev/null
+++ b/ravenwood/services-test/Android.bp
@@ -0,0 +1,21 @@
+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: "RavenwoodServicesTest",
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ ],
+ srcs: [
+ "test/**/*.java",
+ ],
+ auto_gen_config: true,
+}
diff --git a/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesTest.java b/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesTest.java
new file mode 100644
index 0000000..c1dee5d
--- /dev/null
+++ b/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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 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.ravenwood.RavenwoodRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.LocalServices;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodServicesTest {
+ private static final String TEST_VIRTUAL_PORT = "virtual:example";
+
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProcessSystem()
+ .setServicesRequired(SerialManager.class)
+ .build();
+
+ @Test
+ public void testDefined() {
+ final SerialManager fromName = (SerialManager)
+ mRavenwood.getContext().getSystemService(Context.SERIAL_SERVICE);
+ final SerialManager fromClass =
+ mRavenwood.getContext().getSystemService(SerialManager.class);
+ assertNotNull(fromName);
+ assertNotNull(fromClass);
+ assertEquals(fromName, fromClass);
+
+ assertNotNull(LocalServices.getService(SerialManagerInternal.class));
+ }
+
+ @Test
+ 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);
+ }
+
+ @Test
+ public void testDriven() {
+ final SerialManager service = (SerialManager)
+ mRavenwood.getContext().getSystemService(Context.SERIAL_SERVICE);
+ final SerialManagerInternal internal = LocalServices.getService(
+ SerialManagerInternal.class);
+
+ internal.addVirtualSerialPortForTest(TEST_VIRTUAL_PORT, () -> {
+ throw new UnsupportedOperationException(
+ "Needs socketpair() to offer accurate emulation");
+ });
+ final String[] ports = service.getSerialPorts();
+ assertEquals(1, ports.length);
+ assertEquals(TEST_VIRTUAL_PORT, ports[0]);
+ }
+}
diff --git a/ravenwood/services.core-ravenwood-policies.txt b/ravenwood/services.core-ravenwood-policies.txt
new file mode 100644
index 0000000..d8d563e
--- /dev/null
+++ b/ravenwood/services.core-ravenwood-policies.txt
@@ -0,0 +1,7 @@
+# Ravenwood "policy" file for services.core.
+
+# Keep all AIDL interfaces
+class :aidl stubclass
+
+# Keep all feature flag implementations
+class :feature_flags stubclass