summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Jeff Sharkey <jsharkey@google.com> 2024-02-27 12:36:36 -0700
committer Jeff Sharkey <jsharkey@google.com> 2024-02-29 11:04:46 -0700
commit8a922ecadb76c49a62b43fab80507466a92db7cc (patch)
tree4efeff667f1684324ebea2472e769217c03ada56
parent16f7cdf2e01e158efe8d8334a15e9e20e59de929 (diff)
Support service dependencies on Ravenwood.
Ravenwood test authors should only need to call setServicesRequired() for the services they directly interact with, but often those services will have indirect dependencies on other OS internals, and those dependencies will evolve over time. To handle this, we add the concept of dependencies to SystemService, where a service declares the other services required to run. (They can choose to omit services from their dependencies where they gracefully handle a missing service at runtime.) Bug: 325506297 Test: atest RavenwoodServicesTest Change-Id: I42187b3494fef499d619ad882891e832b0fd6bca
-rw-r--r--Ravenwood.bp3
-rw-r--r--ravenwood/Android.bp16
-rw-r--r--ravenwood/framework-src/android/ravenwood/example/BlueManager.java34
-rw-r--r--ravenwood/framework-src/android/ravenwood/example/RedManager.java34
-rw-r--r--ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java20
-rw-r--r--ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java51
-rw-r--r--ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java4
-rw-r--r--ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesDependenciesTest.java54
-rw-r--r--services/core/java/com/android/server/SystemService.java40
-rw-r--r--services/fakes/java/com/android/server/example/BlueManagerService.java44
-rw-r--r--services/fakes/java/com/android/server/example/RedManagerService.java64
11 files changed, 345 insertions, 19 deletions
diff --git a/Ravenwood.bp b/Ravenwood.bp
index c73e04896173..4791640239b3 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -149,6 +149,7 @@ java_library {
installable: false,
srcs: [":services.fakes-sources"],
libs: [
+ "ravenwood-framework",
"services.core.ravenwood",
],
jarjar_rules: ":ravenwood-services-jarjar-rules",
@@ -204,6 +205,7 @@ android_ravenwood_libgroup {
// Provide runtime versions of utils linked in below
"junit",
"truth",
+ "ravenwood-framework",
"ravenwood-junit-impl",
"mockito-ravenwood-prebuilt",
"inline-mockito-ravenwood-prebuilt",
@@ -218,6 +220,7 @@ android_ravenwood_libgroup {
libs: [
"junit",
"truth",
+ "ravenwood-framework",
"ravenwood-junit",
"mockito-ravenwood-prebuilt",
"inline-mockito-ravenwood-prebuilt",
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 41a4a1ad1ccb..e535f0a05a02 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -77,6 +77,7 @@ java_library {
libs: [
"android.test.mock",
"framework-minus-apex.ravenwood",
+ "ravenwood-framework",
"services.core.ravenwood",
"junit",
],
@@ -102,6 +103,21 @@ java_library {
visibility: ["//visibility:public"],
}
+// Library used to publish a handful of `android.ravenwood` APIs into
+// the Ravenwood BCP; we don't want to publish these APIs into the BCP
+// on physical devices, which is why this is a separate library
+java_library {
+ name: "ravenwood-framework",
+ srcs: [
+ "framework-src/**/*.java",
+ ],
+ libs: [
+ "framework-minus-apex.ravenwood",
+ ],
+ sdk_version: "core_current",
+ visibility: ["//visibility:public"],
+}
+
java_host_for_device {
name: "androidx.test.monitor-for-device",
libs: [
diff --git a/ravenwood/framework-src/android/ravenwood/example/BlueManager.java b/ravenwood/framework-src/android/ravenwood/example/BlueManager.java
new file mode 100644
index 000000000000..fc713b1bd164
--- /dev/null
+++ b/ravenwood/framework-src/android/ravenwood/example/BlueManager.java
@@ -0,0 +1,34 @@
+/*
+ * 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.example;
+
+import android.annotation.SystemService;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+@SystemService(BlueManager.SERVICE_NAME)
+public class BlueManager {
+ public static final String SERVICE_NAME = "example_blue";
+
+ public String getInterfaceDescriptor() {
+ try {
+ return ServiceManager.getService(SERVICE_NAME).getInterfaceDescriptor();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/ravenwood/framework-src/android/ravenwood/example/RedManager.java b/ravenwood/framework-src/android/ravenwood/example/RedManager.java
new file mode 100644
index 000000000000..381a901f234a
--- /dev/null
+++ b/ravenwood/framework-src/android/ravenwood/example/RedManager.java
@@ -0,0 +1,34 @@
+/*
+ * 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.example;
+
+import android.annotation.SystemService;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+@SystemService(RedManager.SERVICE_NAME)
+public class RedManager {
+ public static final String SERVICE_NAME = "example_red";
+
+ public String getInterfaceDescriptor() {
+ try {
+ return ServiceManager.getService(SERVICE_NAME).getInterfaceDescriptor();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
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 c17d0903f856..109ef76b535f 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
@@ -26,6 +26,8 @@ import android.os.Looper;
import android.os.PermissionEnforcer;
import android.os.ServiceManager;
import android.os.UserHandle;
+import android.ravenwood.example.BlueManager;
+import android.ravenwood.example.RedManager;
import android.test.mock.MockContext;
import android.util.ArrayMap;
import android.util.Singleton;
@@ -53,16 +55,23 @@ public class RavenwoodContext extends MockContext {
mPackageName = packageName;
mMainThread = mainThread;
+ // Services provided by a typical shipping device
registerService(ClipboardManager.class,
- Context.CLIPBOARD_SERVICE, asSingleton(() ->
+ Context.CLIPBOARD_SERVICE, memoize(() ->
new ClipboardManager(this, getMainThreadHandler())));
registerService(PermissionEnforcer.class,
Context.PERMISSION_ENFORCER_SERVICE, () -> mEnforcer);
registerService(SerialManager.class,
- Context.SERIAL_SERVICE, asSingleton(() ->
+ Context.SERIAL_SERVICE, memoize(() ->
new SerialManager(this, ISerialManager.Stub.asInterface(
ServiceManager.getService(Context.SERIAL_SERVICE)))
));
+
+ // Additional services we provide for testing purposes
+ registerService(BlueManager.class,
+ BlueManager.SERVICE_NAME, memoize(() -> new BlueManager()));
+ registerService(RedManager.class,
+ RedManager.SERVICE_NAME, memoize(() -> new RedManager()));
}
@Override
@@ -143,9 +152,12 @@ public class RavenwoodContext extends MockContext {
}
/**
- * Wrap the given {@link Supplier} to become a memoized singleton.
+ * Wrap the given {@link Supplier} to become memoized.
+ *
+ * The underlying {@link Supplier} will only be invoked once, and that result will be cached
+ * and returned for any future requests.
*/
- private static <T> Supplier<T> asSingleton(ThrowingSupplier<T> supplier) {
+ private static <T> Supplier<T> memoize(ThrowingSupplier<T> supplier) {
final Singleton<T> singleton = new Singleton<>() {
@Override
protected T create() {
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 3de96c0990ea..cd6b61df392f 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
@@ -19,13 +19,19 @@ package android.platform.test.ravenwood;
import android.content.ClipboardManager;
import android.hardware.SerialManager;
import android.os.SystemClock;
+import android.ravenwood.example.BlueManager;
+import android.ravenwood.example.RedManager;
import android.util.ArrayMap;
+import android.util.ArraySet;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import com.android.server.SystemServiceManager;
import com.android.server.utils.TimingsTraceAndSlog;
+import java.util.List;
+import java.util.Set;
+
public class RavenwoodSystemServer {
/**
* Set of services that we know how to provide under Ravenwood. We keep this set distinct
@@ -37,16 +43,21 @@ public class RavenwoodSystemServer {
*/
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 {
+ // Services provided by a typical shipping device
sKnownServices.put(ClipboardManager.class,
"com.android.server.FakeClipboardService$Lifecycle");
sKnownServices.put(SerialManager.class,
"com.android.server.SerialService$Lifecycle");
+
+ // Additional services we provide for testing purposes
+ sKnownServices.put(BlueManager.class,
+ "com.android.server.example.BlueManagerService$Lifecycle");
+ sKnownServices.put(RedManager.class,
+ "com.android.server.example.RedManagerService$Lifecycle");
}
+ private static Set<Class<?>> sStartedServices;
private static TimingsTraceAndSlog sTimings;
private static SystemServiceManager sServiceManager;
@@ -54,6 +65,7 @@ public class RavenwoodSystemServer {
// Avoid overhead if no services required
if (rule.mServicesRequired.isEmpty()) return;
+ sStartedServices = new ArraySet<>();
sTimings = new TimingsTraceAndSlog();
sServiceManager = new SystemServiceManager(rule.mContext);
sServiceManager.setStartInfo(false,
@@ -61,17 +73,7 @@ public class RavenwoodSystemServer {
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);
- }
- }
+ startServices(rule.mServicesRequired);
sServiceManager.sealStartedServices();
// TODO: expand to include additional boot phases when relevant
@@ -85,5 +87,26 @@ public class RavenwoodSystemServer {
LocalServices.removeServiceForTest(SystemServiceManager.class);
sServiceManager = null;
sTimings = null;
+ sStartedServices = null;
+ }
+
+ private static void startServices(List<Class<?>> serviceClasses) {
+ for (Class<?> serviceClass : serviceClasses) {
+ // Quietly ignore duplicate requests if service already started
+ if (sStartedServices.contains(serviceClass)) continue;
+ sStartedServices.add(serviceClass);
+
+ final String serviceName = sKnownServices.get(serviceClass);
+ if (serviceName == null) {
+ throw new RuntimeException("The requested service " + serviceClass
+ + " 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");
+ }
+
+ // Start service and then depth-first traversal of any dependencies
+ final SystemService instance = sServiceManager.startService(serviceName);
+ startServices(instance.getDependencies());
+ }
}
}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index a520d4ccafa1..52ea3402fa62 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -35,6 +35,8 @@ 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;
@@ -127,7 +129,7 @@ public class RavenwoodRule implements TestRule {
final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties();
- final ArraySet<Class<?>> mServicesRequired = new ArraySet<>();
+ final List<Class<?>> mServicesRequired = new ArrayList<>();
volatile Context mContext;
volatile Instrumentation mInstrumentation;
diff --git a/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesDependenciesTest.java b/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesDependenciesTest.java
new file mode 100644
index 000000000000..efe468d0df25
--- /dev/null
+++ b/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesDependenciesTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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 android.platform.test.ravenwood.RavenwoodRule;
+import android.ravenwood.example.BlueManager;
+import android.ravenwood.example.RedManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodServicesDependenciesTest {
+ // NOTE: we carefully only ask for RedManager here, and rely on Ravenwood internals to spin
+ // up the implicit dependency on BlueManager
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProcessSystem()
+ .setServicesRequired(RedManager.class)
+ .build();
+
+ @Test
+ public void testDirect() {
+ final RedManager red = mRavenwood.getContext().getSystemService(
+ RedManager.class);
+ assertEquals("blue+red", red.getInterfaceDescriptor());
+ }
+
+ @Test
+ public void testIndirect() {
+ final BlueManager blue = mRavenwood.getContext().getSystemService(
+ BlueManager.class);
+ assertEquals("blue", blue.getInterfaceDescriptor());
+ }
+}
diff --git a/services/core/java/com/android/server/SystemService.java b/services/core/java/com/android/server/SystemService.java
index 7dc9f10e646c..4de85fe9f2ff 100644
--- a/services/core/java/com/android/server/SystemService.java
+++ b/services/core/java/com/android/server/SystemService.java
@@ -39,7 +39,9 @@ import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+import java.util.Objects;
/**
* The base class for services running in the system process. Override and implement
@@ -135,6 +137,7 @@ public abstract class SystemService {
public @interface BootPhase {}
private final Context mContext;
+ private final List<Class<?>> mDependencies;
/**
* Class representing user in question in the lifecycle callbacks.
@@ -332,7 +335,28 @@ public abstract class SystemService {
* @param context The system server context.
*/
public SystemService(@NonNull Context context) {
+ this(context, Collections.emptyList());
+ }
+
+ /**
+ * Initializes the system service.
+ * <p>
+ * Subclasses must define a single argument constructor that accepts the context
+ * and passes it to super.
+ * </p>
+ *
+ * @param context The system server context.
+ * @param dependencies The list of dependencies that this service requires to operate.
+ * Currently only used by the Ravenwood deviceless testing environment to
+ * understand transitive dependencies needed to support a specific test.
+ * For example, including {@code PowerManager.class} here indicates that
+ * this service requires the {@code PowerManager} and/or {@code
+ * PowerManagerInternal} APIs to function.
+ * @hide
+ */
+ public SystemService(@NonNull Context context, @NonNull List<Class<?>> dependencies) {
mContext = context;
+ mDependencies = Objects.requireNonNull(dependencies);
}
/**
@@ -356,6 +380,22 @@ public abstract class SystemService {
}
/**
+ * Get the list of dependencies that this service requires to operate.
+ *
+ * Currently only used by the Ravenwood deviceless testing environment to understand transitive
+ * dependencies needed to support a specific test.
+ *
+ * For example, including {@code PowerManager.class} here indicates that this service
+ * requires the {@code PowerManager} and/or {@code PowerManagerInternal} APIs to function.
+ *
+ * @hide
+ */
+ @NonNull
+ public final List<Class<?>> getDependencies() {
+ return mDependencies;
+ }
+
+ /**
* Returns true if the system is running in safe mode.
* TODO: we should define in which phase this becomes valid
*
diff --git a/services/fakes/java/com/android/server/example/BlueManagerService.java b/services/fakes/java/com/android/server/example/BlueManagerService.java
new file mode 100644
index 000000000000..8e3c8d73df9c
--- /dev/null
+++ b/services/fakes/java/com/android/server/example/BlueManagerService.java
@@ -0,0 +1,44 @@
+/*
+ * 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.server.example;
+
+import android.content.Context;
+import android.os.Binder;
+import android.ravenwood.example.BlueManager;
+
+import com.android.server.SystemService;
+
+public class BlueManagerService extends Binder {
+ public static class Lifecycle extends SystemService {
+ private BlueManagerService mService;
+
+ public Lifecycle(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ mService = new BlueManagerService();
+ publishBinderService(BlueManager.SERVICE_NAME, mService);
+ }
+ }
+
+ @Override
+ public String getInterfaceDescriptor() {
+ return "blue";
+ }
+}
diff --git a/services/fakes/java/com/android/server/example/RedManagerService.java b/services/fakes/java/com/android/server/example/RedManagerService.java
new file mode 100644
index 000000000000..e0be7337698d
--- /dev/null
+++ b/services/fakes/java/com/android/server/example/RedManagerService.java
@@ -0,0 +1,64 @@
+/*
+ * 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.server.example;
+
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.ravenwood.example.BlueManager;
+import android.ravenwood.example.RedManager;
+
+import com.android.server.SystemService;
+
+import java.util.List;
+
+public class RedManagerService extends Binder {
+ private IBinder mBlueService;
+
+ public static class Lifecycle extends SystemService {
+ private RedManagerService mService;
+
+ public Lifecycle(Context context) {
+ super(context, List.of(BlueManager.class));
+ }
+
+ @Override
+ public void onStart() {
+ mService = new RedManagerService();
+ publishBinderService(RedManager.SERVICE_NAME, mService);
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase == PHASE_SYSTEM_SERVICES_READY) {
+ mService.mBlueService = getBinderService(BlueManager.SERVICE_NAME);
+ }
+ }
+ }
+
+ @Override
+ public String getInterfaceDescriptor() {
+ try {
+ // Obtain the answer from dependency, but then augment it to prove that the answer
+ // was channeled through us
+ return mBlueService.getInterfaceDescriptor() + "+red";
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}