[fuchsia_modular_test] add TestHarnessSpecBuilder

TEST=
- fx run-test fuchsia_modular_test_package_integration_tests

Change-Id: I8fcc06826e782371824cad91d7a976d843081177
diff --git a/public/dart/fuchsia_modular_test/BUILD.gn b/public/dart/fuchsia_modular_test/BUILD.gn
index 8df7c5a..9e3370c 100644
--- a/public/dart/fuchsia_modular_test/BUILD.gn
+++ b/public/dart/fuchsia_modular_test/BUILD.gn
@@ -14,8 +14,9 @@
   sdk_category = "partner"
 
   sources = [
-    "test.dart",
     "src/test_harness_fixtures.dart",
+    "src/test_harness_spec_builder.dart",
+    "test.dart",
   ]
 
   deps = [
@@ -30,6 +31,7 @@
     "//topaz/public/dart/fuchsia_modular",
     "//topaz/public/dart/fuchsia_services",
     "//topaz/public/dart/zircon",
+    "//zircon/public/fidl/fuchsia-mem",
   ]
 }
 
@@ -45,6 +47,7 @@
 
   sources = [
     "launch_harness_test.dart",
+    "test_harness_spec_builder_test.dart",
   ]
 
   deps = [
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 b12876d..33e440d 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
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:math';
 
 import 'package:fidl_fuchsia_modular_testing/fidl_async.dart' as fidl_testing;
 import 'package:fidl_fuchsia_sys/fidl_async.dart' as fidl_sys;
@@ -40,3 +41,10 @@
 
   return harness;
 }
+
+/// Generates a random component url with the correct format
+String generateComponentUrl() {
+  final rand = Random();
+  final name = List.generate(10, (_) => rand.nextInt(9).toString()).join('');
+  return 'fuchsia-pkg://example.com/$name#meta/$name.cmx';
+}
diff --git a/public/dart/fuchsia_modular_test/lib/src/test_harness_spec_builder.dart b/public/dart/fuchsia_modular_test/lib/src/test_harness_spec_builder.dart
new file mode 100644
index 0000000..9cc02aa
--- /dev/null
+++ b/public/dart/fuchsia_modular_test/lib/src/test_harness_spec_builder.dart
@@ -0,0 +1,65 @@
+// 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:convert' show utf8, json;
+
+import 'package:fidl_fuchsia_mem/fidl_async.dart' as fuchsia_mem;
+import 'package:fidl_fuchsia_modular_testing/fidl_async.dart';
+import 'package:zircon/zircon.dart';
+
+/// A class which aids in the building of [TestHarnessSpec] objects.
+///
+/// This class is used to build up the spec and then pass that to the
+/// run method of the test harness.
+/// ```
+/// final builder = TestHarnessBuilder()
+///   ..addComponentToIntercept(componentUrl);
+/// await harness.run(builder.build());
+/// ```
+class TestHarnessSpecBuilder {
+  final _componentsToIntercept = <InterceptSpec>[];
+
+  /// Registers the component url to be intercepted.
+  ///
+  /// When a component with the given [componentUrl] is launched inside the
+  /// hermetic environment it will not be launched by the system but rather
+  /// passed to the [TestHarness]'s onNewComponent stream.
+  ///
+  /// Optionally, additional [services] can be provided which will be added
+  /// to the intercepted components cmx file.
+  void addComponentToIntercept(String componentUrl, {List<String> services}) {
+    ArgumentError.checkNotNull(componentUrl, 'componentUrl');
+
+    // verify that we have unique component urls
+    for (final spec in _componentsToIntercept) {
+      if (spec.componentUrl == componentUrl) {
+        throw Exception(
+            'Attempting to add [$componentUrl] twice. Component urls must be unique');
+      }
+    }
+
+    final extraContents = <String, dynamic>{};
+    if (services != null) {
+      extraContents['services'] = services;
+    }
+    _componentsToIntercept.add(InterceptSpec(
+        componentUrl: componentUrl,
+        extraCmxContents: _createCmxSandBox(extraContents)));
+  }
+
+  /// Returns the [TestHarnessSpec] object which can be passed to the [TestHarnessProxy]
+  TestHarnessSpec build() {
+    return TestHarnessSpec(componentsToIntercept: _componentsToIntercept);
+  }
+
+  fuchsia_mem.Buffer _createCmxSandBox(Map<String, dynamic> contents) {
+    if (contents.isEmpty) {
+      return null;
+    }
+    final encodedContents = utf8.encode(json.encode({'sandbox': contents}));
+
+    final vmo = SizedVmo.fromUint8List(encodedContents);
+    return fuchsia_mem.Buffer(vmo: vmo, size: encodedContents.length);
+  }
+}
diff --git a/public/dart/fuchsia_modular_test/lib/test.dart b/public/dart/fuchsia_modular_test/lib/test.dart
index 4a347d2..059ce58 100644
--- a/public/dart/fuchsia_modular_test/lib/test.dart
+++ b/public/dart/fuchsia_modular_test/lib/test.dart
@@ -5,3 +5,4 @@
 /// A collection of utilities to help with writing tests
 /// against the fuchsia_modular package.
 export 'src/test_harness_fixtures.dart';
+export 'src/test_harness_spec_builder.dart';
diff --git a/public/dart/fuchsia_modular_test/test/test_harness_spec_builder_test.dart b/public/dart/fuchsia_modular_test/test/test_harness_spec_builder_test.dart
new file mode 100644
index 0000000..b7b2291
--- /dev/null
+++ b/public/dart/fuchsia_modular_test/test/test_harness_spec_builder_test.dart
@@ -0,0 +1,71 @@
+// 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:convert';
+
+// ignore_for_file: implementation_imports
+import 'package:test/test.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:fuchsia_modular_test/src/test_harness_spec_builder.dart';
+import 'package:fuchsia_modular_test/src/test_harness_fixtures.dart';
+import 'package:zircon/zircon.dart';
+
+void main() {
+  setupLogger();
+
+  group('spec builder', () {
+    TestHarnessSpecBuilder builder;
+
+    setUp(() {
+      builder = TestHarnessSpecBuilder();
+    });
+
+    test('addComponentToIntercept fails on null component url', () {
+      expect(() => builder.addComponentToIntercept(null), throwsArgumentError);
+    });
+
+    test('addComponentToIntercept adds to the spec', () {
+      final url = generateComponentUrl();
+      builder.addComponentToIntercept(url);
+
+      final spec = builder.build();
+      expect(spec.componentsToIntercept,
+          contains(predicate((ispec) => ispec.componentUrl == url)));
+    });
+
+    test('able to add many components', () {
+      builder
+        ..addComponentToIntercept(generateComponentUrl())
+        ..addComponentToIntercept(generateComponentUrl());
+
+      expect(builder.build().componentsToIntercept.length, 2);
+    });
+
+    test('componentUrl can only be added once', () {
+      final url = generateComponentUrl();
+      builder.addComponentToIntercept(url);
+      expect(() => builder.addComponentToIntercept(url), throwsException);
+    });
+
+    test('can add services to the components cmx file', () {
+      const service = 'fuchsia.sys.Launcher';
+      builder
+          .addComponentToIntercept(generateComponentUrl(), services: [service]);
+
+      final spec = builder.build();
+      final interceptSpec = spec.componentsToIntercept.first;
+      final contents = interceptSpec.extraCmxContents;
+      final vmo = SizedVmo(contents.vmo.handle, contents.size);
+      final bytes = vmo.read(contents.size).bytesAsUint8List();
+
+      final expectedSandbox = {
+        'sandbox': {
+          'services': [service]
+        }
+      };
+
+      expect(expectedSandbox, json.decode(utf8.decode(bytes)));
+    });
+  });
+}