[fuchsia_modular_test] add ability to intercept agents
TEST=
- fx run-test fuchsia_modular_test_package_integration_tests
Change-Id: I70142800b72a6d98923f3a9527b4edd5bb4451ec
diff --git a/public/dart/fuchsia_modular/lib/src/lifecycle/internal/_lifecycle_impl.dart b/public/dart/fuchsia_modular/lib/src/lifecycle/internal/_lifecycle_impl.dart
index d425576..a791986 100644
--- a/public/dart/fuchsia_modular/lib/src/lifecycle/internal/_lifecycle_impl.dart
+++ b/public/dart/fuchsia_modular/lib/src/lifecycle/internal/_lifecycle_impl.dart
@@ -32,8 +32,8 @@
final _terminateListeners = <Future<void> Function()>{};
/// Initializes this [LifecycleImpl] instance
- LifecycleImpl() {
- _exposeService();
+ LifecycleImpl({StartupContext context}) {
+ _exposeService(context ?? StartupContext.fromStartupInfo());
}
/// Adds a terminate [listener] which will be called when the system starts to
@@ -62,8 +62,7 @@
// Exposes this instance to the [StartupContext#outgoingServices].
//
// This class be must called before the first iteration of the event loop.
- void _exposeService() {
- StartupContext startupContext = StartupContext.fromStartupInfo();
+ void _exposeService(StartupContext startupContext) {
startupContext.outgoing.addPublicService(
(InterfaceRequest<fidl.Lifecycle> request) {
_lifecycleBinding.bind(this, request);
diff --git a/public/dart/fuchsia_modular_test/BUILD.gn b/public/dart/fuchsia_modular_test/BUILD.gn
index 9e3370c..ecaaeb2 100644
--- a/public/dart/fuchsia_modular_test/BUILD.gn
+++ b/public/dart/fuchsia_modular_test/BUILD.gn
@@ -14,6 +14,7 @@
sdk_category = "partner"
sources = [
+ "src/agent_interceptor.dart",
"src/test_harness_fixtures.dart",
"src/test_harness_spec_builder.dart",
"test.dart",
@@ -28,6 +29,7 @@
"//third_party/dart-pkg/pub/test_api",
"//topaz/public/dart/fidl",
"//topaz/public/dart/fuchsia",
+ "//topaz/public/dart/fuchsia_logger",
"//topaz/public/dart/fuchsia_modular",
"//topaz/public/dart/fuchsia_services",
"//topaz/public/dart/zircon",
@@ -35,6 +37,15 @@
]
}
+
+fidl("test_fidl") {
+ name = "test.modular.dart"
+
+ sources = [
+ "test_support/fidl/testing.fidl",
+ ]
+}
+
# Run tese tests using:
# fx run-test fuchsia_modular_test_package_integration_tests
dart_fuchsia_test("fuchsia_modular_test_package_integration_tests") {
@@ -46,12 +57,14 @@
]
sources = [
+ "agent_interceptor_test.dart",
"launch_harness_test.dart",
"test_harness_spec_builder_test.dart",
]
deps = [
":fuchsia_modular_test",
+ ":test_fidl",
"//third_party/dart-pkg/pub/test",
]
environments = basic_envs
diff --git a/public/dart/fuchsia_modular_test/lib/src/agent_interceptor.dart b/public/dart/fuchsia_modular_test/lib/src/agent_interceptor.dart
new file mode 100644
index 0000000..5723faa
--- /dev/null
+++ b/public/dart/fuchsia_modular_test/lib/src/agent_interceptor.dart
@@ -0,0 +1,113 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:fuchsia_modular/agent.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fidl/fidl.dart';
+import 'package:fidl_fuchsia_modular_testing/fidl_async.dart';
+import 'package:fidl_fuchsia_sys/fidl_async.dart' as fidl_sys;
+import 'package:fuchsia_modular/src/agent/internal/_agent_impl.dart'; // ignore: implementation_imports
+import 'package:fuchsia_services/src/internal/_startup_context_impl.dart'; // ignore: implementation_imports
+import 'package:fuchsia_modular/src/lifecycle/internal/_lifecycle_impl.dart'; // ignore: implementation_imports
+
+/// A function which is called when a new agent that is being launched.
+typedef OnNewAgent = void Function(Agent agent);
+
+/// A helper class for managing the intercepting of agents inside the
+/// [TestHarness].
+///
+/// When agents which are registered to be mocked are launchedm the [OnNewAgent]
+/// function will be executed allowing developers to expose services.
+///
+/// ```
+/// AgentInterceptor(testHarness.onNewComponent)
+/// .mockAgent(agentUrl, (agent) {
+/// agent.exposeService(myService);
+/// });
+///
+/// connectToAgentService(agentUrl, myServiceProxy,
+/// componentContextProxy, await getComponentContext(harness));
+/// ```
+class AgentInterceptor {
+ final _registeredAgents = <String, OnNewAgent>{};
+ final _mockedAgents = <String, _MockedAgent>{};
+
+ /// Creates an instance of this which will listen to the [onNewComponentStream].
+ AgentInterceptor(
+ Stream<TestHarness$OnNewComponent$Response> onNewComponentStream)
+ : assert(onNewComponentStream != null) {
+ onNewComponentStream.listen(_handleResponse);
+ }
+
+ /// Register an [agentUrl] to be mocked.
+ ///
+ /// If a component with the component url which matches [agentUrl] is
+ /// registered to be interecepted by the test harness [onNewAgent] will be
+ /// called when that component is first launched. The [onNewAgent] method will
+ /// be called with an injected [Agent] object. This method can be treated like
+ /// a normal main method in a non mocked agent.
+ void mockAgent(String agentUrl, OnNewAgent onNewAgent) {
+ ArgumentError.checkNotNull(agentUrl, 'agentUrl');
+ ArgumentError.checkNotNull(onNewAgent, 'onNewAgent');
+
+ if (agentUrl.isEmpty) {
+ throw ArgumentError('agentUrl must not be empty');
+ }
+
+ if (_registeredAgents.containsKey(agentUrl)) {
+ throw Exception(
+ 'Attempting to add [$agentUrl] twice. Agent urls must be unique');
+ }
+ _registeredAgents[agentUrl] = onNewAgent;
+ }
+
+ /// This method is called by the listen method when this object is used as the
+ /// handler to the [TestHarnessProxy.onNewComponent] stream.
+ void _handleResponse(TestHarness$OnNewComponent$Response response) {
+ final startupInfo = response.startupInfo;
+ final componentUrl = startupInfo.launchInfo.url;
+ if (_registeredAgents.containsKey(componentUrl)) {
+ final mockedAgent = _MockedAgent(
+ startupInfo: startupInfo,
+ interceptedComponentRequest: response.interceptedComponent,
+ );
+ _mockedAgents[componentUrl] = mockedAgent;
+ _registeredAgents[componentUrl](mockedAgent.agent);
+ } else {
+ log.info(
+ 'Skipping launched component [$componentUrl] because it was not registered');
+ }
+ }
+}
+
+/// A helper class which helps manage the lifecyle of a mocked agent
+class _MockedAgent {
+ /// The intercepted component. This object can be used to control the
+ /// launched component.
+ final InterceptedComponentProxy interceptedComponent =
+ InterceptedComponentProxy();
+
+ /// The instance of the [Agent] which is running in this environment
+ AgentImpl agent;
+
+ /// The startup context for this environment
+ StartupContextImpl context;
+
+ /// The lifecycle service for this environment
+ LifecycleImpl lifecycle;
+
+ _MockedAgent({
+ fidl_sys.StartupInfo startupInfo,
+ InterfaceHandle<InterceptedComponent> interceptedComponentRequest,
+ }) {
+ context = StartupContextImpl.from(startupInfo);
+ agent = AgentImpl(startupContext: context);
+ lifecycle = LifecycleImpl(context: context)
+ ..addTerminateListener(() async {
+ interceptedComponent.ctrl.close();
+ });
+
+ interceptedComponent.ctrl.bind(interceptedComponentRequest);
+ }
+}
diff --git a/public/dart/fuchsia_modular_test/lib/src/test_harness_fixtures.dart b/public/dart/fuchsia_modular_test/lib/src/test_harness_fixtures.dart
index 33e440d..7f2c950 100644
--- a/public/dart/fuchsia_modular_test/lib/src/test_harness_fixtures.dart
+++ b/public/dart/fuchsia_modular_test/lib/src/test_harness_fixtures.dart
@@ -6,6 +6,7 @@
import 'dart:math';
import 'package:fidl_fuchsia_modular_testing/fidl_async.dart' as fidl_testing;
+import 'package:fidl_fuchsia_modular/fidl_async.dart' as fidl_modular;
import 'package:fidl_fuchsia_sys/fidl_async.dart' as fidl_sys;
import 'package:fuchsia_services/services.dart';
@@ -29,15 +30,17 @@
directoryRequest: incoming.request().passChannel());
await launcher.createComponent(
launchInfo, componentControllerProxy.ctrl.request());
-
launcher.ctrl.close();
// hold a reference to the componentControllerProxy so it lives as long as the
// harness and does not kill the service if it is garbage collected.
// ignore: unawaited_futures
- harness.ctrl.whenClosed.then((_) => componentControllerProxy.ctrl.close());
+ harness.ctrl.whenClosed.then((_) {
+ componentControllerProxy.ctrl.close();
+ });
incoming.connectToService(harness);
+ await incoming.close();
return harness;
}
@@ -48,3 +51,13 @@
final name = List.generate(10, (_) => rand.nextInt(9).toString()).join('');
return 'fuchsia-pkg://example.com/$name#meta/$name.cmx';
}
+
+/// Returns the connection to [fidl_modular.ComponentContextProxy] which is
+/// running inside the [harness]'s hermetic environment
+Future<fidl_modular.ComponentContextProxy> getComponentContext(
+ fidl_testing.TestHarnessProxy harness) async {
+ final proxy = fidl_modular.ComponentContextProxy();
+ await harness.connectToModularService(
+ fidl_testing.ModularService.withComponentContext(proxy.ctrl.request()));
+ return proxy;
+}
diff --git a/public/dart/fuchsia_modular_test/lib/test.dart b/public/dart/fuchsia_modular_test/lib/test.dart
index 059ce58..2268073 100644
--- a/public/dart/fuchsia_modular_test/lib/test.dart
+++ b/public/dart/fuchsia_modular_test/lib/test.dart
@@ -4,5 +4,6 @@
/// A collection of utilities to help with writing tests
/// against the fuchsia_modular package.
+export 'src/agent_interceptor.dart';
export 'src/test_harness_fixtures.dart';
export 'src/test_harness_spec_builder.dart';
diff --git a/public/dart/fuchsia_modular_test/test/agent_interceptor_test.dart b/public/dart/fuchsia_modular_test/test/agent_interceptor_test.dart
new file mode 100644
index 0000000..4dd8a0f
--- /dev/null
+++ b/public/dart/fuchsia_modular_test/test/agent_interceptor_test.dart
@@ -0,0 +1,117 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+// ignore_for_file: implementation_imports
+import 'package:fuchsia_modular_test/src/test_harness_spec_builder.dart';
+import 'package:fuchsia_modular_test/test.dart';
+import 'package:test/test.dart';
+import 'package:fidl_fuchsia_modular_testing/fidl_async.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fuchsia_modular_test/src/agent_interceptor.dart';
+import 'package:fuchsia_modular_test/src/test_harness_fixtures.dart';
+import 'package:fuchsia_modular/service_connection.dart';
+import 'package:fidl_test_modular_dart/fidl_async.dart';
+
+void main() {
+ setupLogger();
+
+ group('mock registration', () {
+ AgentInterceptor agentInterceptor;
+
+ setUp(() {
+ agentInterceptor =
+ AgentInterceptor(Stream<TestHarness$OnNewComponent$Response>.empty());
+ });
+
+ test('mockAgent throws for null agentUrl', () {
+ expect(
+ () => agentInterceptor.mockAgent(null, (_) {}), throwsArgumentError);
+ });
+
+ test('mockAgent throws for empty agentUrl', () {
+ expect(() => agentInterceptor.mockAgent('', (_) {}), throwsArgumentError);
+ });
+
+ test('mockAgent throws for missing callback', () {
+ expect(() => agentInterceptor.mockAgent(generateComponentUrl(), null),
+ throwsArgumentError);
+ });
+
+ test('mockAgent throws for registering agent twice', () {
+ final agentUrl = generateComponentUrl();
+ void callback(_) {}
+
+ agentInterceptor.mockAgent(agentUrl, callback);
+
+ expect(() => agentInterceptor.mockAgent(agentUrl, callback),
+ throwsException);
+ });
+ });
+
+ group('agent intercepting', () {
+ TestHarnessProxy harness;
+ TestHarnessSpec spec;
+ String agentUrl;
+
+ setUp(() async {
+ agentUrl = generateComponentUrl();
+ harness = await launchTestHarness();
+ spec =
+ (TestHarnessSpecBuilder()..addComponentToIntercept(agentUrl)).build();
+ });
+
+ tearDown(() {
+ harness.ctrl.close();
+ });
+
+ test('onNewAgent called for mocked agent', () async {
+ final didCallCompleter = Completer<bool>();
+ AgentInterceptor(harness.onNewComponent).mockAgent(agentUrl, (agent) {
+ expect(agent, isNotNull);
+ didCallCompleter.complete(true);
+ });
+
+ await harness.run(spec);
+
+ final componentContext = await getComponentContext(harness);
+ final proxy = ServerProxy();
+ connectToAgentService(agentUrl, proxy,
+ componentContextProxy: componentContext);
+ componentContext.ctrl.close();
+ proxy.ctrl.close();
+ expect(await didCallCompleter.future, isTrue);
+ });
+
+ test('onNewAgent can expose a service', () async {
+ final spec =
+ (TestHarnessSpecBuilder()..addComponentToIntercept(agentUrl)).build();
+
+ final server = _ServerImpl();
+ AgentInterceptor(harness.onNewComponent).mockAgent(agentUrl, (agent) {
+ agent.exposeService(server);
+ });
+
+ await harness.run(spec);
+
+ final fooProxy = ServerProxy();
+ final componentContext = await getComponentContext(harness);
+ connectToAgentService(agentUrl, fooProxy,
+ componentContextProxy: componentContext);
+
+ expect(await fooProxy.echo('some value'), 'some value');
+
+ fooProxy.ctrl.close();
+ componentContext.ctrl.close();
+ });
+ });
+}
+
+class _ServerImpl extends Server {
+ @override
+ Future<String> echo(String value) async {
+ return value;
+ }
+}
diff --git a/public/dart/fuchsia_modular_test/test/launch_harness_test.dart b/public/dart/fuchsia_modular_test/test/launch_harness_test.dart
index 36078b5..267b012 100644
--- a/public/dart/fuchsia_modular_test/test/launch_harness_test.dart
+++ b/public/dart/fuchsia_modular_test/test/launch_harness_test.dart
@@ -40,6 +40,7 @@
final storyPuppetMaster = fidl_modular.StoryPuppetMasterProxy();
await puppetMaster.controlStory('foo', storyPuppetMaster.ctrl.request());
+ puppetMaster.ctrl.close();
final addMod = fidl_modular.AddMod(
intent: fidl_modular.Intent(action: '', handler: url),
@@ -51,6 +52,7 @@
await storyPuppetMaster
.enqueue([fidl_modular.StoryCommand.withAddMod(addMod)]);
await storyPuppetMaster.execute();
+ storyPuppetMaster.ctrl.close();
});
});
}
diff --git a/public/dart/fuchsia_modular_test/test_support/fidl/testing.fidl b/public/dart/fuchsia_modular_test/test_support/fidl/testing.fidl
new file mode 100644
index 0000000..6b5fb4f
--- /dev/null
+++ b/public/dart/fuchsia_modular_test/test_support/fidl/testing.fidl
@@ -0,0 +1,10 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library test.modular.dart;
+
+[Discoverable]
+protocol Server {
+ echo(string? value) -> (string? response);
+};
diff --git a/public/dart/fuchsia_services/lib/src/incoming.dart b/public/dart/fuchsia_services/lib/src/incoming.dart
index 8178101..4e15fd8 100644
--- a/public/dart/fuchsia_services/lib/src/incoming.dart
+++ b/public/dart/fuchsia_services/lib/src/incoming.dart
@@ -47,7 +47,9 @@
/// Terminates connection and return Zircon status.
Future<int> close() async {
- return _dirProxy.close();
+ final status = await _dirProxy.close();
+ _dirProxy.ctrl.close();
+ return status;
}
/// Connects to the incoming service specified by [serviceProxy].