[modular][testing] Test harness component & gtest fixture

This CL introduces two features:
* A component that provides the TestHarness as a service.
  (`modular_test_harness.cmx`)
* A gtest fixture that exposes TestHarness using
  `modular_test_harness.cmx`.

modular_test_harness.cmx is intended to be used by test fixtures in
other languages such as Dart and C++.

Also in this CL:
* TestHarnessImpl will accept and invoke an exit callback if the test
  harness becomes unavailable.

Test: `modular_test_harness_test` exercises the test fixture, which also
exercises modular_test_harness.cmx

Change-Id: I58a4d0f80e218fca6bf896d0a1a1843f39e21764
diff --git a/peridot/bin/modular_test_harness/BUILD.gn b/peridot/bin/modular_test_harness/BUILD.gn
index 8104455..352ac25 100644
--- a/peridot/bin/modular_test_harness/BUILD.gn
+++ b/peridot/bin/modular_test_harness/BUILD.gn
@@ -4,6 +4,22 @@
 
 import("//build/package.gni")
 
+executable("modular_test_harness_bin") {
+  testonly = true
+
+  sources = [
+    "modular_test_harness.cc",
+  ]
+
+  deps = [
+    "//garnet/public/lib/fxl",
+    "//sdk/lib/sys/cpp",
+    "//sdk/fidl/fuchsia.modular.testing",
+    "//peridot/public/lib/modular_test_harness/cpp:test_harness_impl",
+    "//zircon/public/lib/async-loop-cpp",
+  ]
+}
+
 executable("test_base_shell_bin") {
   testonly = true
 
@@ -59,6 +75,20 @@
   ]
 }
 
+executable("modular_test_harness_test") {
+  testonly = true
+  output_name = "modular_test_harness_test"
+  sources = ["modular_test_harness_test.cc"]
+  deps = [
+    "//sdk/lib/sys/cpp",
+    "//sdk/lib/sys/cpp/testing:integration",
+    "//sdk/fidl/fuchsia.sys",
+    "//sdk/fidl/fuchsia.modular.testing",
+    "//peridot/public/lib/modular_test_harness/cpp",
+    "//third_party/googletest:gtest_main",
+  ]
+}
+
 package("modular_test_harness") {
   testonly = true
 
@@ -72,10 +102,28 @@
     {
       name = "test_session_shell_bin"
     },
+    {
+      name = "modular_test_harness_bin"
+    },
+  ]
+
+  tests = [
+    {
+      name = "modular_test_harness_test"
+      environments = basic_envs
+    },
   ]
 
   meta = [
     {
+      path = "meta/modular_test_harness.cmx"
+      dest = "modular_test_harness.cmx"
+    },
+    {
+      path = "meta/modular_test_harness_test.cmx"
+      dest = "modular_test_harness_test.cmx"
+    },
+    {
       path = "meta/test_base_shell.cmx"
       dest = "test_base_shell.cmx"
     },
@@ -90,8 +138,10 @@
   ]
 
   deps = [
+    ":modular_test_harness_bin",
+    ":modular_test_harness_test",
     ":test_base_shell_bin",
-    ":test_session_shell_bin",
     ":test_story_shell_bin",
+    ":test_session_shell_bin",
   ]
 }
diff --git a/peridot/bin/modular_test_harness/meta/modular_test_harness.cmx b/peridot/bin/modular_test_harness/meta/modular_test_harness.cmx
new file mode 100644
index 0000000..2735eeb
--- /dev/null
+++ b/peridot/bin/modular_test_harness/meta/modular_test_harness.cmx
@@ -0,0 +1,11 @@
+{
+    "program": {
+        "binary": "bin/modular_test_harness_bin"
+    },
+    "sandbox": {
+        "services": [
+            "fuchsia.sys.Environment",
+            "fuchsia.sys.Loader"
+        ]
+    }
+}
\ No newline at end of file
diff --git a/peridot/bin/modular_test_harness/meta/modular_test_harness_test.cmx b/peridot/bin/modular_test_harness/meta/modular_test_harness_test.cmx
new file mode 100644
index 0000000..b66c96f7
--- /dev/null
+++ b/peridot/bin/modular_test_harness/meta/modular_test_harness_test.cmx
@@ -0,0 +1,11 @@
+{
+    "program": {
+        "binary": "test/modular_test_harness_test"
+    },
+    "sandbox": {
+        "services": [
+            "fuchsia.sys.Environment",
+            "fuchsia.sys.Launcher"
+        ]
+    }
+}
\ No newline at end of file
diff --git a/peridot/bin/modular_test_harness/modular_test_harness.cc b/peridot/bin/modular_test_harness/modular_test_harness.cc
new file mode 100644
index 0000000..6b3772c
--- /dev/null
+++ b/peridot/bin/modular_test_harness/modular_test_harness.cc
@@ -0,0 +1,32 @@
+// 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.
+
+// This component provides the |fuchsia.modular.testing.TestHarness| fidl
+// service. This component will exit if the test harness becomes unavailable.
+
+#include <lib/async-loop/cpp/loop.h>
+#include <lib/modular_test_harness/cpp/test_harness_impl.h>
+#include <sdk/lib/sys/cpp/component_context.h>
+#include <src/lib/fxl/logging.h>
+
+#include <memory>
+
+int main(int argc, const char** argv) {
+  async::Loop loop(&kAsyncLoopConfigAttachToThread);
+
+  std::unique_ptr<modular::testing::TestHarnessImpl> test_harness_impl;
+
+  auto context = sys::ComponentContext::Create();
+  auto env = context->svc()->Connect<fuchsia::sys::Environment>();
+  context->outgoing()->AddPublicService<fuchsia::modular::testing::TestHarness>(
+      [&loop, &context, &env, &test_harness_impl](
+          fidl::InterfaceRequest<fuchsia::modular::testing::TestHarness>
+              request) {
+        test_harness_impl = std::make_unique<modular::testing::TestHarnessImpl>(
+            env, std::move(request), [&loop] { loop.Quit(); });
+      });
+
+  loop.Run();
+  return 0;
+}
diff --git a/peridot/bin/modular_test_harness/modular_test_harness_test.cc b/peridot/bin/modular_test_harness/modular_test_harness_test.cc
new file mode 100644
index 0000000..646308b
--- /dev/null
+++ b/peridot/bin/modular_test_harness/modular_test_harness_test.cc
@@ -0,0 +1,36 @@
+// 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.
+
+#include <fuchsia/modular/testing/cpp/fidl.h>
+#include <lib/modular_test_harness/cpp/test_harness_fixture.h>
+#include <sdk/lib/sys/cpp/service_directory.h>
+#include <sdk/lib/sys/cpp/testing/test_with_environment.h>
+
+class TestHarnessFixtureTest : public modular::testing::TestHarnessFixture {};
+
+// Ensure that the TestHarnessFixture is able to launch the modular runtime.
+TEST_F(TestHarnessFixtureTest, SimpleSuccess) {
+  constexpr char kFakeBaseShellUrl[] =
+      "fuchsia-pkg://example.com/FAKE_BASE_SHELL_PKG/fake_base_shell.cmx";
+
+  fuchsia::modular::testing::InterceptSpec shell_intercept_spec;
+  shell_intercept_spec.set_component_url(kFakeBaseShellUrl);
+  fuchsia::modular::testing::TestHarnessSpec spec;
+  spec.mutable_base_shell()->set_intercept_spec(
+      std::move(shell_intercept_spec));
+
+  // Listen for base shell interception.
+  bool intercepted = false;
+
+  test_harness().events().OnNewBaseShell =
+      [&intercepted](
+          fuchsia::sys::StartupInfo startup_info,
+          fidl::InterfaceHandle<fuchsia::modular::testing::InterceptedComponent>
+              component) { intercepted = true; };
+
+  test_harness()->Run(std::move(spec));
+
+  ASSERT_TRUE(
+      RunLoopWithTimeoutOrUntil([&] { return intercepted; }, zx::sec(5)));
+}
\ No newline at end of file
diff --git a/peridot/public/lib/modular_test_harness/cpp/BUILD.gn b/peridot/public/lib/modular_test_harness/cpp/BUILD.gn
index 865ac6b..c86de66 100644
--- a/peridot/public/lib/modular_test_harness/cpp/BUILD.gn
+++ b/peridot/public/lib/modular_test_harness/cpp/BUILD.gn
@@ -6,7 +6,7 @@
   testonly = true
 
   public_deps = [
-    ":test_harness_impl",
+    ":test_harness_fixture",
   ]
 }
 
@@ -18,6 +18,22 @@
   ]
 }
 
+# This library has a run-time dependency on the `modular_test_harness` package.
+source_set("test_harness_fixture") {
+  testonly = true
+
+  sources = [
+    "test_harness_fixture.h",
+    "test_harness_fixture.cc"
+  ]
+
+  deps = [
+    "//sdk/fidl/fuchsia.modular.testing",
+    "//sdk/lib/sys/cpp",
+    "//sdk/lib/sys/cpp/testing:integration",
+  ]
+}
+
 source_set("test_harness_impl") {
   testonly = true
 
@@ -49,6 +65,8 @@
 
   deps = [
     ":test_harness_impl",
+    "//sdk/fidl/fuchsia.modular",
+    "//sdk/fidl/fuchsia.modular.testing",
     "//sdk/lib/sys/cpp/testing:integration",
     "//sdk/lib/sys/cpp/testing:unit",
     "//third_party/googletest:gtest_main",
diff --git a/peridot/public/lib/modular_test_harness/cpp/test_harness_fixture.cc b/peridot/public/lib/modular_test_harness/cpp/test_harness_fixture.cc
new file mode 100644
index 0000000..01dd236
--- /dev/null
+++ b/peridot/public/lib/modular_test_harness/cpp/test_harness_fixture.cc
@@ -0,0 +1,31 @@
+// 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.
+
+#include "lib/modular_test_harness/cpp/test_harness_fixture.h"
+
+namespace modular {
+namespace testing {
+namespace {
+
+const char kTestHarnessUrl[] =
+    "fuchsia-pkg://fuchsia.com/modular_test_harness#meta/"
+    "modular_test_harness.cmx";
+
+}  // namespace
+
+TestHarnessFixture::TestHarnessFixture() {
+  fuchsia::sys::LaunchInfo launch_info;
+  launch_info.url = kTestHarnessUrl;
+  svc_ =
+      sys::ServiceDirectory::CreateWithRequest(&launch_info.directory_request);
+  launcher_ptr()->CreateComponent(std::move(launch_info),
+                                  test_harness_ctrl_.NewRequest());
+
+  test_harness_ = svc_->Connect<fuchsia::modular::testing::TestHarness>();
+}
+
+TestHarnessFixture::~TestHarnessFixture() = default;
+
+}  // namespace testing
+}  // namespace modular
diff --git a/peridot/public/lib/modular_test_harness/cpp/test_harness_fixture.h b/peridot/public/lib/modular_test_harness/cpp/test_harness_fixture.h
new file mode 100644
index 0000000..c86b577
--- /dev/null
+++ b/peridot/public/lib/modular_test_harness/cpp/test_harness_fixture.h
@@ -0,0 +1,35 @@
+// 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.
+
+#ifndef LIB_MODULAR_TEST_HARNESS_CPP_TEST_HARNESS_FIXTURE_H_
+#define LIB_MODULAR_TEST_HARNESS_CPP_TEST_HARNESS_FIXTURE_H_
+
+#include <fuchsia/modular/testing/cpp/fidl.h>
+#include <sdk/lib/sys/cpp/service_directory.h>
+#include <sdk/lib/sys/cpp/testing/test_with_environment.h>
+
+namespace modular {
+namespace testing {
+
+// A gtest fixture for tests that require an instance of the modular runtime.
+// This fixture requires the `modular_test_harness` package to be available.
+class TestHarnessFixture : public sys::testing::TestWithEnvironment {
+ protected:
+  TestHarnessFixture();
+  virtual ~TestHarnessFixture();
+
+  fuchsia::modular::testing::TestHarnessPtr& test_harness() {
+    return test_harness_;
+  }
+
+ private:
+  std::shared_ptr<sys::ServiceDirectory> svc_;
+  fuchsia::modular::testing::TestHarnessPtr test_harness_;
+  fuchsia::sys::ComponentControllerPtr test_harness_ctrl_;
+};
+
+}  // namespace testing
+}  // namespace modular
+
+#endif  // LIB_MODULAR_TEST_HARNESS_CPP_TEST_HARNESS_FIXTURE_H_
diff --git a/peridot/public/lib/modular_test_harness/cpp/test_harness_impl.cc b/peridot/public/lib/modular_test_harness/cpp/test_harness_impl.cc
index 2329b3f..4cd1f2f 100644
--- a/peridot/public/lib/modular_test_harness/cpp/test_harness_impl.cc
+++ b/peridot/public/lib/modular_test_harness/cpp/test_harness_impl.cc
@@ -96,12 +96,17 @@
 
 TestHarnessImpl::TestHarnessImpl(
     const fuchsia::sys::EnvironmentPtr& parent_env,
-    fidl::InterfaceRequest<fuchsia::modular::testing::TestHarness> request)
+    fidl::InterfaceRequest<fuchsia::modular::testing::TestHarness> request,
+    fit::function<void()> on_disconnected)
     : parent_env_(parent_env),
       binding_(this, std::move(request)),
+      on_disconnected_(std::move(on_disconnected)),
       interceptor_(
           sys::testing::ComponentInterceptor::CreateWithEnvironmentLoader(
-              parent_env_)) {}
+              parent_env_)) {
+  binding_.set_error_handler(
+      [this](zx_status_t status) { CloseBindingIfError(status); });
+}
 
 TestHarnessImpl::~TestHarnessImpl() = default;
 
@@ -136,6 +141,7 @@
     binding_.Close(status);
     // destory |enclosing_env_| should kill all processes.
     enclosing_env_.reset();
+    on_disconnected_();
     return true;
   }
   return false;
diff --git a/peridot/public/lib/modular_test_harness/cpp/test_harness_impl.h b/peridot/public/lib/modular_test_harness/cpp/test_harness_impl.h
index 937e39f..4406227 100644
--- a/peridot/public/lib/modular_test_harness/cpp/test_harness_impl.h
+++ b/peridot/public/lib/modular_test_harness/cpp/test_harness_impl.h
@@ -26,8 +26,15 @@
   //
   // |test_harness_request| is implemented by this class. The TestHarness
   // FIDL interface is the way to interact with the TestHarness API.
+  //
+  // |on_exit| is called when the TestHarness interface is closed.
+  // This can happen if the TestHarness client drops their side of the
+  // connection, or this class closes it due to an error; In this case, the
+  // error is sent as an epitaph. See the |TestHarness| protocol documentation
+  // for more details.
   TestHarnessImpl(const fuchsia::sys::EnvironmentPtr& parent_env,
-                  fidl::InterfaceRequest<TestHarness> test_harness_request);
+                  fidl::InterfaceRequest<TestHarness> test_harness_request,
+                  fit::function<void()> on_disconnected);
 
   virtual ~TestHarnessImpl() override;
 
@@ -133,6 +140,8 @@
   fidl::Binding<fuchsia::modular::testing::TestHarness> binding_;
   fuchsia::modular::testing::TestHarnessSpec spec_;
 
+  fit::function<void()> on_disconnected_;
+
   // This map manages InterceptedComponent bindings (and their implementations).
   // When a |InterceptedComponent| connection is closed, it is automatically
   // removed from this map (and its impl is deleted as well).
diff --git a/peridot/public/lib/modular_test_harness/cpp/test_harness_impl_unittest.cc b/peridot/public/lib/modular_test_harness/cpp/test_harness_impl_unittest.cc
index a928615..9c36700 100644
--- a/peridot/public/lib/modular_test_harness/cpp/test_harness_impl_unittest.cc
+++ b/peridot/public/lib/modular_test_harness/cpp/test_harness_impl_unittest.cc
@@ -23,24 +23,35 @@
 
 class TestHarnessImplTest : public sys::testing::TestWithEnvironment {
  public:
-  TestHarnessImplTest() : harness_impl_(real_env(), harness_.NewRequest()) {}
+  TestHarnessImplTest()
+      : harness_impl_(real_env(), harness_.NewRequest(),
+                      [this] { did_exit_ = true; }) {}
 
-  const fuchsia::modular::testing::TestHarnessPtr& test_harness() {
+  fuchsia::modular::testing::TestHarnessPtr& test_harness() {
     return harness_;
   };
 
+  bool did_exit() { return did_exit_; }
+
   std::vector<std::string> MakeBasemgrArgs(
       fuchsia::modular::testing::TestHarnessSpec spec) {
     return TestHarnessImpl::MakeBasemgrArgs(std::move(spec));
   }
 
  private:
+  bool did_exit_ = false;
   fuchsia::modular::testing::TestHarnessPtr harness_;
   ::modular::testing::TestHarnessImpl harness_impl_;
 };
 
 namespace {
 
+TEST_F(TestHarnessImplTest, ExitCallback) {
+  test_harness().Unbind();
+  ASSERT_TRUE(
+      RunLoopWithTimeoutOrUntil([&] { return did_exit(); }, zx::sec(5)));
+}
+
 TEST_F(TestHarnessImplTest, DefaultMakeBasemgrArgs) {
   std::vector<std::string> expected = {
       "--test",
@@ -123,20 +134,16 @@
         std::move(intercept_spec));
   }
 
-  fuchsia::modular::testing::TestHarnessPtr harness;
-  ::modular::testing::TestHarnessImpl harness_impl(real_env(),
-                                                   harness.NewRequest());
-
   // Listen for story shell interception.
   bool story_shell_intercepted = false;
-  harness.events().OnNewStoryShell =
+  test_harness().events().OnNewStoryShell =
       [&](fuchsia::sys::StartupInfo startup_info,
           fidl::InterfaceHandle<fuchsia::modular::testing::InterceptedComponent>
               component) { story_shell_intercepted = true; };
 
   // Listen for module interception.
   bool fake_module_intercepted = false;
-  harness.events().OnNewComponent =
+  test_harness().events().OnNewComponent =
       [&](fuchsia::sys::StartupInfo startup_info,
           fidl::InterfaceHandle<fuchsia::modular::testing::InterceptedComponent>
               component) {
@@ -144,7 +151,7 @@
           fake_module_intercepted = true;
         }
       };
-  harness->Run(std::move(spec));
+  test_harness()->Run(std::move(spec));
 
   // Create a new story -- this should auto-start the story (because of
   // test_session_shell's behaviour), and launch a new story shell.
@@ -153,7 +160,7 @@
 
   fuchsia::modular::testing::TestHarnessService svc;
   svc.set_puppet_master(puppet_master.NewRequest());
-  harness->GetService(std::move(svc));
+  test_harness()->GetService(std::move(svc));
 
   puppet_master->ControlStory("my_story", story_master.NewRequest());