[modular][testing] Teach TestHarness to inject a service directory into environment

Refactor:
* |TestHarnesSpec.env_config| provides a way for tests to configure what
goes into the environment. This deprecates the |services_to_inherit| and
|services_to_inject| fields.

Changes:
* |TestHarnessSpec.env_config.service_dir| is a directory of services a
test can provide to the test harness environment.
TestHarness.ConnectToEnvironmentService() can be used to connect to the
env services.

Test:
* PseudoDirServerTest tests OpenAt() and Serve()
* TestHarnessImplTest.EnvironmentServices tests that the test-provided
directory of services are available in the test harness environment.

MF-390 #done

Change-Id: I717313db03493b292d5cd7b79788f56d3d2e5278
diff --git a/peridot/bin/modular_test_harness/modular_test_harness_test.cc b/peridot/bin/modular_test_harness/modular_test_harness_test.cc
index eaf7f68..f4c5f62 100644
--- a/peridot/bin/modular_test_harness/modular_test_harness_test.cc
+++ b/peridot/bin/modular_test_harness/modular_test_harness_test.cc
@@ -7,10 +7,10 @@
 #include <sdk/lib/sys/cpp/service_directory.h>
 #include <sdk/lib/sys/cpp/testing/test_with_environment.h>
 
-class TestHarnessFixtureTest : public modular::testing::TestHarnessFixture {};
+class ModularTestHarnessTest : public modular::testing::TestHarnessFixture {};
 
 // Ensure that the TestHarnessFixture is able to launch the modular runtime.
-TEST_F(TestHarnessFixtureTest, SimpleSuccess) {
+TEST_F(ModularTestHarnessTest, SimpleSuccess) {
   constexpr char kFakeBaseShellUrl[] =
       "fuchsia-pkg://example.com/FAKE_BASE_SHELL_PKG/fake_base_shell.cmx";
 
diff --git a/peridot/lib/util/BUILD.gn b/peridot/lib/util/BUILD.gn
index c773e38c..f28c915 100644
--- a/peridot/lib/util/BUILD.gn
+++ b/peridot/lib/util/BUILD.gn
@@ -58,13 +58,13 @@
   testonly = true
 
   sources = [
+    "pseudo_dir_server_unittest.cc",
     "pseudo_dir_utils_unittest.cc",
     "string_escape_unittest.cc",
   ]
 
   public_deps = [
     ":util",
-    "//peridot/lib/testing:test_with_ledger",
     "//src/lib/fxl",
     "//third_party/googletest:gtest",
   ]
diff --git a/peridot/lib/util/pseudo_dir_server.cc b/peridot/lib/util/pseudo_dir_server.cc
index 65439d9..5a4be8f 100644
--- a/peridot/lib/util/pseudo_dir_server.cc
+++ b/peridot/lib/util/pseudo_dir_server.cc
@@ -4,6 +4,8 @@
 
 #include "peridot/lib/util/pseudo_dir_server.h"
 
+#include <lib/async/cpp/task.h>
+
 namespace modular {
 
 PseudoDirServer::PseudoDirServer(std::unique_ptr<vfs::PseudoDir> pseudo_dir)
@@ -37,6 +39,18 @@
   return fsl::OpenChannelAsFileDescriptor(node.Unbind().TakeChannel());
 }
 
+fuchsia::io::DirectoryPtr PseudoDirServer::Serve() {
+  fuchsia::io::DirectoryPtr directory;
+  auto req = directory.NewRequest().TakeChannel();
+  async::PostTask(
+      thread_loop_->dispatcher(), [this, req = std::move(req)]() mutable {
+        pseudo_dir_->Serve(
+            fuchsia::io::OPEN_RIGHT_READABLE | fuchsia::io::OPEN_RIGHT_WRITABLE,
+            std::move(req));
+      });
+  return directory;
+}
+
 // This method is the handler for a new thread. It lets the owning thread know
 // that it has started and serves a directory requests. The thread is exited
 // when this object is destroyed.
diff --git a/peridot/lib/util/pseudo_dir_server.h b/peridot/lib/util/pseudo_dir_server.h
index bb56879..b5a40ca 100644
--- a/peridot/lib/util/pseudo_dir_server.h
+++ b/peridot/lib/util/pseudo_dir_server.h
@@ -36,9 +36,13 @@
   // This destructor blocks the current thread until the child thread exits.
   ~PseudoDirServer();
 
-  // Opens a read-only FD at |path|.  Path must not begin with '/'.
+  // Opens a read-only FD at |path|.  |path| must not lead with a '/'.
   fxl::UniqueFD OpenAt(std::string path);
 
+  // Returns a directory connection for this pseudo dir. This directory is
+  // served on a different thread than the caller's thread.
+  fuchsia::io::DirectoryPtr Serve();
+
  private:
   // This method is the handler for a new thread. It lets the owning thread
   // know that it has started and serves a directory requests. The thread is
@@ -46,8 +50,9 @@
   void StartThread(fidl::InterfaceRequest<fuchsia::io::Directory> request);
 
   std::unique_ptr<vfs::PseudoDir> pseudo_dir_;
-  // The directory connection we that |pseudo_dir| serves over in a differnt
-  // thread.
+
+  // The directory connection which |OpenAt()| uses. This directory connection
+  // is served on |serving_thread_|'s thread.
   fuchsia::io::DirectoryPtr dir_;
 
   // The mutex & condition variable are used by the new thread (owned by
diff --git a/peridot/lib/util/pseudo_dir_server_unittest.cc b/peridot/lib/util/pseudo_dir_server_unittest.cc
new file mode 100644
index 0000000..5958e1b
--- /dev/null
+++ b/peridot/lib/util/pseudo_dir_server_unittest.cc
@@ -0,0 +1,68 @@
+// Copyright 2017 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 "peridot/lib/util/pseudo_dir_server.h"
+
+#include <gtest/gtest.h>
+#include <lib/gtest/real_loop_fixture.h>
+#include <peridot/lib/util/pseudo_dir_utils.h>
+#include <src/lib/files/directory.h>
+#include <src/lib/files/file.h>
+
+namespace modular {
+namespace {
+
+class PseudoDirServerTest : public gtest::RealLoopFixture {};
+
+// Test that |PseudoDirServer::OpenAt| gives you an FD can be used on the same
+// thread as |PseudoDirServer|.
+TEST_F(PseudoDirServerTest, OpenAt) {
+  constexpr char kContents[] = "file contents";
+  modular::PseudoDirServer server(MakeFilePathWithContents("a/b/c", kContents));
+
+  {
+    // Paths with leading '/' don't work.
+    fxl::UniqueFD fd = server.OpenAt("/a");
+    EXPECT_FALSE(fd.is_valid());
+  }
+  {
+    // 'x' doesn't exist, so not valid.
+    fxl::UniqueFD fd = server.OpenAt("x");
+    EXPECT_FALSE(fd.is_valid());
+  }
+  {
+    fxl::UniqueFD fd = server.OpenAt("a");
+    EXPECT_TRUE(fd.is_valid());
+  }
+  {
+    fxl::UniqueFD fd = server.OpenAt("a/b");
+    EXPECT_TRUE(fd.is_valid());
+  }
+  {
+    fxl::UniqueFD fd = server.OpenAt("a/b/c");
+    EXPECT_TRUE(fd.is_valid());
+
+    std::string contents;
+    ASSERT_TRUE(files::ReadFileDescriptorToString(fd.get(), &contents));
+    EXPECT_EQ(kContents, contents);
+  }
+}
+
+// Test that |PseudoDirServer::Serve| serves a directory which doesn't block the
+// current thread. Test this by using thread-blocking POSIX apis.
+TEST_F(PseudoDirServerTest, Serve) {
+  constexpr char kFileName[] = "file_name";
+  constexpr char kContents[] = "file contents";
+  modular::PseudoDirServer server(
+      MakeFilePathWithContents(kFileName, kContents));
+  auto dir_fd =
+      fsl::OpenChannelAsFileDescriptor(server.Serve().Unbind().TakeChannel());
+
+  std::string contents;
+  ASSERT_TRUE(files::ReadFileToStringAt(dir_fd.get(), kFileName, &contents));
+  EXPECT_EQ(kContents, contents);
+}
+
+}  // namespace
+}  // namespace modular
diff --git a/peridot/lib/util/pseudo_dir_utils_unittest.cc b/peridot/lib/util/pseudo_dir_utils_unittest.cc
index 651e640..174e2cd 100644
--- a/peridot/lib/util/pseudo_dir_utils_unittest.cc
+++ b/peridot/lib/util/pseudo_dir_utils_unittest.cc
@@ -2,10 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "peridot/lib/util/pseudo_dir_utils.h"
+
 #include <gtest/gtest.h>
-#include <peridot/lib/testing/test_with_ledger.h>
+#include <lib/gtest/real_loop_fixture.h>
 #include <peridot/lib/util/pseudo_dir_server.h>
-#include <peridot/lib/util/pseudo_dir_utils.h>
 #include <src/lib/files/directory.h>
 #include <src/lib/files/file.h>
 
@@ -14,7 +15,7 @@
 
 constexpr int kDefaultBufferSize = 1024;
 
-class PseudoDirUtilsTest : public testing::TestWithLedger {
+class PseudoDirUtilsTest : public gtest::RealLoopFixture {
  public:
   PseudoDirUtilsTest() = default;
   ~PseudoDirUtilsTest() override = default;
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 2d6f4e1..34e49f9e 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
@@ -4,6 +4,9 @@
 
 #include "lib/modular_test_harness/cpp/test_harness_impl.h"
 
+#include <dirent.h>
+#include <lib/fdio/directory.h>
+#include <lib/fsl/io/fd.h>
 #include <lib/fsl/vmo/strings.h>
 #include <lib/vfs/cpp/pseudo_dir.h>
 #include <lib/vfs/cpp/pseudo_file.h>
@@ -11,10 +14,12 @@
 #include <peridot/lib/modular_config/modular_config_xdr.h>
 #include <peridot/lib/util/pseudo_dir_utils.h>
 #include <src/lib/files/path.h>
+#include <src/lib/files/unique_fd.h>
 #include <src/lib/fxl/logging.h>
 #include <src/lib/fxl/strings/join_strings.h>
 #include <src/lib/fxl/strings/split_string.h>
 #include <src/lib/fxl/strings/substitute.h>
+#include <zircon/status.h>
 
 namespace modular::testing {
 namespace {
@@ -171,6 +176,8 @@
 
 bool TestHarnessImpl::CloseBindingIfError(zx_status_t status) {
   if (status != ZX_OK) {
+    FXL_LOG(ERROR) << "Destroying TestHarness because of error: "
+                   << zx_status_get_string(status);
     binding_.Close(status);
     // destory |enclosing_env_| should kill all processes.
     enclosing_env_.reset();
@@ -180,30 +187,6 @@
   return false;
 }
 
-void TestHarnessImpl::InjectServicesIntoEnvironment(
-    sys::testing::EnvironmentServices* env_services,
-    std::map<std::string, std::string>* default_injected_svcs) {
-  // Wire up client-specified injected services, and remove them from the
-  // default injected services.
-  if (spec_.has_env_services_to_inject()) {
-    for (const auto& injected_svc : spec_.env_services_to_inject()) {
-      default_injected_svcs->erase(injected_svc.name);
-
-      fuchsia::sys::LaunchInfo info;
-      info.url = injected_svc.url;
-      env_services->AddServiceWithLaunchInfo(std::move(info),
-                                             injected_svc.name);
-    }
-  }
-
-  // Wire up the remaining default injected services.
-  for (const auto& injected_svc : *default_injected_svcs) {
-    fuchsia::sys::LaunchInfo info;
-    info.url = injected_svc.second;
-    env_services->AddServiceWithLaunchInfo(std::move(info), injected_svc.first);
-  }
-}
-
 std::string MakeTestHarnessEnvironmentName() {
   // Apply a random suffix to the environment name so that multiple hermetic
   // test harness environments may coexist under the same parent env.
@@ -213,6 +196,150 @@
                          std::to_string(random_env_suffix));
 }
 
+zx_status_t TestHarnessImpl::PopulateEnvServices(
+    sys::testing::EnvironmentServices* env_services) {
+  // The default set of component-provided services are all basemgr's hard
+  // dependencies. A map of service name => component URL providing the service.
+  std::map<std::string, std::string> default_svcs = {
+      {fuchsia::auth::account::AccountManager::Name_,
+       "fuchsia-pkg://fuchsia.com/account_manager#meta/account_manager.cmx"},
+      {fuchsia::devicesettings::DeviceSettingsManager::Name_,
+       "fuchsia-pkg://fuchsia.com/device_settings_manager#meta/"
+       "device_settings_manager.cmx"}};
+
+  std::set<std::string> added_svcs;
+
+  // 1. Allow services to be inherited from parent environment.
+  if (spec_.has_env_services_to_inherit()) {
+    for (auto& svc_name : spec_.env_services_to_inherit()) {
+      added_svcs.insert(svc_name);
+      env_services->AllowParentService(svc_name);
+    }
+  }
+
+  // 2. Inject component-provided services.
+  if (auto retval = PopulateEnvServicesWithComponents(env_services,
+                                                      &added_svcs) != ZX_OK) {
+    return retval;
+  }
+
+  // 3. Inject service_dir services.
+  if (auto retval = PopulateEnvServicesWithServiceDir(env_services,
+                                                      &added_svcs) != ZX_OK) {
+    return retval;
+  }
+
+  // 4. Inject the remaining default component-provided services.
+  for (const auto& svc_component : default_svcs) {
+    if (added_svcs.find(svc_component.first) != added_svcs.end()) {
+      continue;
+    }
+    fuchsia::sys::LaunchInfo info;
+    info.url = svc_component.second;
+    env_services->AddServiceWithLaunchInfo(std::move(info),
+                                           svc_component.first);
+  }
+
+  return ZX_OK;
+}
+
+zx_status_t TestHarnessImpl::PopulateEnvServicesWithComponents(
+    sys::testing::EnvironmentServices* env_services,
+    std::set<std::string>* added_svcs) {
+  // Wire up client-specified injected services, and remove them from the
+  // default injected services.
+  if (spec_.has_env_services_to_inject()) {
+    for (const auto& svc : spec_.env_services_to_inject()) {
+      if (added_svcs->find(svc.name) != added_svcs->end()) {
+        FXL_LOG(ERROR) << svc.name
+                       << " has already been injected into the environment, "
+                          "cannot add twice.";
+        return ZX_ERR_ALREADY_EXISTS;
+      }
+      added_svcs->insert(svc.name);
+
+      fuchsia::sys::LaunchInfo info;
+      info.url = svc.url;
+      env_services->AddServiceWithLaunchInfo(std::move(info), svc.name);
+    }
+  }
+
+  if (!spec_.has_env_services() ||
+      !spec_.env_services().has_services_from_components()) {
+    return ZX_OK;
+  }
+  for (const auto& svc : spec_.env_services().services_from_components()) {
+    if (added_svcs->find(svc.name) != added_svcs->end()) {
+      FXL_LOG(ERROR) << svc.name
+                     << " has already been injected into the environment, "
+                        "cannot add twice.";
+      return ZX_ERR_ALREADY_EXISTS;
+    }
+    added_svcs->insert(svc.name);
+
+    fuchsia::sys::LaunchInfo info;
+    info.url = svc.url;
+    env_services->AddServiceWithLaunchInfo(std::move(info), svc.name);
+  }
+  return ZX_OK;
+}  // namespace modular::testing
+
+std::vector<std::string> GetDirListing(fuchsia::io::Directory* dir) {
+  // Make a clone of |dir| since translating to a POSIX fd is destructive.
+  fuchsia::io::NodePtr dir_copy;
+  dir->Clone(fuchsia::io::OPEN_RIGHT_READABLE, dir_copy.NewRequest());
+
+  std::vector<std::string> svcs;
+  DIR* fd = fdopendir(
+      fsl::OpenChannelAsFileDescriptor(dir_copy.Unbind().TakeChannel())
+          .release());
+  FXL_CHECK(fd != nullptr);
+
+  struct dirent* dp = nullptr;
+  while ((dp = readdir(fd)) != nullptr) {
+    if (dp->d_name[0] != '.') {
+      svcs.push_back(dp->d_name);
+    }
+  }
+
+  closedir(fd);
+  return svcs;
+}
+
+zx_status_t TestHarnessImpl::PopulateEnvServicesWithServiceDir(
+    sys::testing::EnvironmentServices* env_services,
+    std::set<std::string>* added_svcs) {
+  if (!spec_.has_env_services() || !spec_.env_services().has_service_dir() ||
+      !spec_.env_services().service_dir()) {
+    return ZX_OK;
+  }
+
+  fuchsia::io::DirectoryPtr dir;
+  dir.Bind(std::move(*spec_.mutable_env_services()->mutable_service_dir()));
+  for (auto& svc_name : GetDirListing(dir.get())) {
+    if (added_svcs->find(svc_name) != added_svcs->end()) {
+      FXL_LOG(ERROR)
+          << svc_name
+          << " is already injected into the environment, cannot add twice.";
+      return ZX_ERR_ALREADY_EXISTS;
+    }
+    env_services->AddService(
+        std::make_unique<vfs::Service>([this, svc_name](
+                                           zx::channel request,
+                                           async_dispatcher_t* dispatcher) {
+          FXL_CHECK(env_service_dir_->Connect(svc_name, std::move(request)) ==
+                    ZX_OK);
+        }),
+        svc_name);
+    added_svcs->insert(svc_name);
+  }
+
+  env_service_dir_ =
+      std::make_unique<sys::ServiceDirectory>(dir.Unbind().TakeChannel());
+
+  return ZX_OK;
+}
+
 void TestHarnessImpl::Run(fuchsia::modular::testing::TestHarnessSpec spec) {
   // Run() can only be called once.
   if (enclosing_env_) {
@@ -232,25 +359,10 @@
   std::unique_ptr<sys::testing::EnvironmentServices> env_services =
       interceptor_.MakeEnvironmentServices(parent_env_);
 
-  // The default injected services are all basemgr's hard dependencies.
-  // A map of service name => component URL serving it.
-  std::map<std::string, std::string> default_injected_svcs = {
-      {fuchsia::auth::account::AccountManager::Name_,
-       "fuchsia-pkg://fuchsia.com/account_manager#meta/account_manager.cmx"},
-      {fuchsia::devicesettings::DeviceSettingsManager::Name_,
-       "fuchsia-pkg://fuchsia.com/device_settings_manager#meta/"
-       "device_settings_manager.cmx"}};
-
-  // Allow services to be inherited from outside the test harness environment.
-  if (spec_.has_env_services_to_inherit()) {
-    for (auto& svc_name : spec_.env_services_to_inherit()) {
-      default_injected_svcs.erase(svc_name);
-      env_services->AllowParentService(svc_name);
-    }
+  if (CloseBindingIfError(PopulateEnvServices(env_services.get()))) {
+    return;
   }
 
-  InjectServicesIntoEnvironment(env_services.get(), &default_injected_svcs);
-
   // Ledger configuration for tests by default:
   // * use a memory-backed FS for ledger.
   // * doesn't sync with a cloudprovider.
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 c2a4d75..565beb9 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
@@ -139,6 +139,28 @@
   // Processes the service requests which are buffered from |GetService()|.
   void FlushBufferedSessionAgentServices();
 
+  // Populates the test harness environment with services described by
+  // |spec_.env_services|.
+  zx_status_t PopulateEnvServices(
+      sys::testing::EnvironmentServices* env_services);
+
+  // Injects services into the test harness environment according to
+  // |spec_.env_services.services_from_components| and
+  // |spec_.env_services_to_inject|.
+  //
+  // Injected service names are inserted into |added_svcs|.
+  zx_status_t PopulateEnvServicesWithComponents(
+      sys::testing::EnvironmentServices* env_services,
+      std::set<std::string>* added_svcs);
+
+  // Injects services into the test harness environment according to
+  // |spec_.env_services.service_dir|.
+  //
+  // Injected service names are inserted into |added_svcs|.
+  zx_status_t PopulateEnvServicesWithServiceDir(
+      sys::testing::EnvironmentServices* env_services,
+      std::set<std::string>* added_svcs);
+
   // The test harness environment is a child of |parent_env_|.
   const fuchsia::sys::EnvironmentPtr& parent_env_;  // Not owned.
 
@@ -147,9 +169,9 @@
 
   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).
+  // 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).
   //
   // The key is the raw-pointer backing the unique_ptr value.
   std::map<InterceptedComponentImpl*, std::unique_ptr<InterceptedComponentImpl>>
@@ -163,6 +185,8 @@
 
   InterceptedSessionAgentInfo intercepted_session_agent_info_;
 
+  std::unique_ptr<sys::ServiceDirectory> env_service_dir_;
+
   friend class TestHarnessImplTest;
 };
 
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 17cc280..50470fb 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
@@ -122,8 +122,8 @@
 
   auto generated_accountmgr_url = GenerateFakeUrl();
 
-  spec.mutable_env_services_to_inject()->push_back(
-      fuchsia::modular::testing::InjectedService{
+  spec.mutable_env_services()->mutable_services_from_components()->push_back(
+      fuchsia::modular::testing::ComponentService{
           // Override the default injected AccountManager.
           .name = fuchsia::auth::account::AccountManager::Name_,
           .url = generated_accountmgr_url});
@@ -161,13 +161,13 @@
 // Test that additional injected services are made available, spin up the
 // associated component when requested. This test exercises injecting a custom
 // service.
-TEST_F(TestHarnessImplTest, CustomInjectedServices) {
+TEST_F(TestHarnessImplTest, ComponentProvidedService) {
   fuchsia::modular::testing::TestHarnessSpec spec;
 
   auto generated_componentctx_url = GenerateFakeUrl();
 
-  spec.mutable_env_services_to_inject()->push_back(
-      fuchsia::modular::testing::InjectedService{
+  spec.mutable_env_services()->mutable_services_from_components()->push_back(
+      fuchsia::modular::testing::ComponentService{
           // Provide a custom injected service.
           .name = fuchsia::modular::ComponentContext::Name_,
           .url = generated_componentctx_url});
@@ -333,6 +333,33 @@
                                         zx::sec(10)));
 };
 
+// Tests that services in |TestHarnessSpec.env_services.service_dir| are
+// accessible in the test harness environment.
+TEST_F(TestHarnessImplTest, EnvironmentServiceDirectory) {
+  constexpr char kTestServiceName[] = "my.test.service";
+
+  bool svc_requested = false;
+  auto svc_dir = std::make_unique<vfs::PseudoDir>();
+  svc_dir->AddEntry(kTestServiceName,
+                    std::make_unique<vfs::Service>(
+                        [&svc_requested](zx::channel request,
+                                         async_dispatcher_t* dispatcher) {
+                          svc_requested = true;
+                        }));
+
+  PseudoDirServer svc_dir_server(std::move(svc_dir));
+
+  fuchsia::modular::testing::TestHarnessSpec spec;
+  spec.mutable_env_services()->set_service_dir(
+      svc_dir_server.Serve().Unbind().TakeChannel());
+  test_harness()->Run(std::move(spec));
+
+  fuchsia::io::NodePtr node;
+  test_harness()->ConnectToEnvironmentService(kTestServiceName,
+                                              node.NewRequest().TakeChannel());
+  RunLoopUntil([&] { return svc_requested; });
+}
+
 }  // namespace
 }  // namespace testing
 }  // namespace modular
diff --git a/sdk/fidl/fuchsia.modular.testing/fuchsia.modular.testing.api b/sdk/fidl/fuchsia.modular.testing/fuchsia.modular.testing.api
index dccda00..b410f1e 100644
--- a/sdk/fidl/fuchsia.modular.testing/fuchsia.modular.testing.api
+++ b/sdk/fidl/fuchsia.modular.testing/fuchsia.modular.testing.api
@@ -1,3 +1,3 @@
 {
-  "fidl/fuchsia.modular.testing/test_harness.fidl": "e173bbde49a937ff14db4acebf865d46"
+  "fidl/fuchsia.modular.testing/test_harness.fidl": "79439b4fe93a9f61a6477750fd515694"
 }
\ No newline at end of file
diff --git a/sdk/fidl/fuchsia.modular.testing/test_harness.fidl b/sdk/fidl/fuchsia.modular.testing/test_harness.fidl
index e52b84b..5350d1a 100644
--- a/sdk/fidl/fuchsia.modular.testing/test_harness.fidl
+++ b/sdk/fidl/fuchsia.modular.testing/test_harness.fidl
@@ -23,6 +23,8 @@
 /// * ZX_ERR_INVALID_ARGS: Run() failed to execute succesfully.
 /// * ZX_ERR_BAD_STATE: Other methods are called before Run() is called.
 /// * ZX_ERR_ALREADY_BOUND: Run() was already called.
+/// * ZX_ERR_ALREADY_EXISTS: The same environment service is being provided
+///   twice.
 [Discoverable]
 protocol TestHarness {
     /// Initializes an instance of the modular runtime in an enclosed
@@ -32,6 +34,8 @@
     /// This protocol connection is closed if Run() fails, with the following
     /// epitaphs:
     ///  * ZX_ERR_INVALID_ARGS: |spec| is mal-formed.
+    ///  * ZX_ERR_ALREADY_EXISTS: The same environment service is being provided
+    ///    twice in |spec.env_services|
     ///  * ZX_ERR_ALREADY_BOUND: Run() was already called.
     Run(TestHarnessSpec spec);
 
@@ -60,7 +64,7 @@
     /// environment.
     ConnectToEnvironmentService(string service_name, handle<channel> request);
 
-    /// DEPRECATED. Use |ConnectToModularService()| instead, they are the same.
+    /// DEPRECATED. Use |ConnectToModularService()| instead.
     GetService(TestHarnessService service);
 };
 
@@ -126,35 +130,51 @@
     /// defaults.
     2: fuchsia.modular.session.SessionmgrConfig sessionmgr_config;
 
-    /// The test harness starts the modular runtime in a hermetic environment
-    /// which does not allow accessing services outside of the environment,
-    /// unless the service names are specified in this list.
-    ///
-    /// For example, adding "fuchsia.net.SocketProvider" here allows the
-    /// hermetic environment to inherit the test process' environment's instance
-    /// of the socket provider. This lets the test code, and the code under
-    /// test, to share the same SocketProvider.
-    3: vector<string> env_services_to_inherit;
-
-    /// A list of services and their providers to inject into this environment,
-    /// along with components which provide them. Multiple services may be
-    /// provided by the same component, but only one instance of the component
-    /// is launched to serve its services. Components are started when one of
-    /// their services is requested, and are kept alive for the duration of the
-    /// test harness environment's life.
-    5: vector<InjectedService> env_services_to_inject;
-
     /// List of component URLs (and additional .cmx contents) to intercept.
     4: vector<InterceptSpec> components_to_intercept;
+
+    /// Options to configure the test harness environment. Use this to inject
+    /// services into the environment.
+    ///
+    /// Optional.
+    6: EnvironmentServicesSpec env_services;
+
+    /// DEPRECATED. Use |env_services.service_dir| to pass through services from
+    /// parent environment.
+    3: vector<string> env_services_to_inherit;
+
+    /// DEPRECATED. Use |env_services.services_from_components| instead.
+    5: vector<InjectedService> env_services_to_inject;
 };
 
-/// Describes a single injected service.
-struct InjectedService {
+/// Options for configuring the test harness environment with services.
+///
+/// If the same service is provided in more than one place, |TestHarness|
+/// connection is closed with a ZX_ERR_ALREADY_EXISTS epitaph.
+table EnvironmentServicesSpec {
+    /// A directory of services to be provided to the test harness environment.
+    ///
+    /// Optional.
+    1: handle<channel> service_dir;
+
+    /// A list of services provided by components to inject into the test
+    /// harness environment. Multiple services may be provided by the same
+    /// component, but only one instance of the component is launched to serve
+    /// its services. Components are started when one of their services is
+    /// requested, and are kept alive for the duration of the test harness
+    /// environment's life.
+    ///
+    /// Optional.
+    2: vector<ComponentService> services_from_components;
+};
+
+/// Describes a service to be provided by a component instance.
+struct ComponentService {
     /// Name of the service.
     string name;
 
-    /// URL of the component which will provide the service (denoted by |name|).
-    /// The service is retrieved from the component's /out/svc namespace.
+    /// URL of the component which will provide the service.
+    /// The service is retrieved from this component's /out/svc namespace.
     fuchsia.sys.component_url url;
 };
 
@@ -188,3 +208,14 @@
     request<fuchsia.modular.ComponentContext> component_context;
     request<fuchsia.modular.AgentContext> agent_context;
 };
+
+/// DEPRECATED. Renamed to ComponentService.
+struct InjectedService {
+    /// Name of the service.
+    string name;
+
+    /// URL of the component which will provide the service.
+    /// The service is retrieved from this component's /out/svc namespace.
+    fuchsia.sys.component_url url;
+};
+