// Copyright 2013 The Chromium 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 "content/browser/service_worker/embedded_worker_instance.h"

#include <stdint.h>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "content/browser/service_worker/embedded_worker_registry.h"
#include "content/browser/service_worker/embedded_worker_status.h"
#include "content/browser/service_worker/embedded_worker_test_helper.h"
#include "content/browser/service_worker/service_worker_context_core.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/browser/service_worker/service_worker_test_utils.h"
#include "content/common/service_worker/embedded_worker.mojom.h"
#include "content/common/service_worker/embedded_worker_messages.h"
#include "content/common/service_worker/embedded_worker_start_params.h"
#include "content/common/service_worker/service_worker_utils.h"
#include "content/public/common/child_process_host.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "mojo/public/cpp/bindings/strong_binding.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace content {

namespace {

void SaveStatusAndCall(ServiceWorkerStatusCode* out,
                       const base::Closure& callback,
                       ServiceWorkerStatusCode status) {
  *out = status;
  callback.Run();
}

std::unique_ptr<EmbeddedWorkerStartParams>
CreateStartParams(int version_id, const GURL& scope, const GURL& script_url) {
  std::unique_ptr<EmbeddedWorkerStartParams> params(
      new EmbeddedWorkerStartParams);
  params->service_worker_version_id = version_id;
  params->scope = scope;
  params->script_url = script_url;
  params->pause_after_download = false;
  params->is_installed = false;
  return params;
}

}  // namespace

class EmbeddedWorkerInstanceTest : public testing::Test,
                                   public EmbeddedWorkerInstance::Listener {
 protected:
  EmbeddedWorkerInstanceTest()
      : thread_bundle_(TestBrowserThreadBundle::IO_MAINLOOP) {}

  enum EventType {
    PROCESS_ALLOCATED,
    START_WORKER_MESSAGE_SENT,
    STARTED,
    STOPPED,
    DETACHED,
  };

  struct EventLog {
    EventType type;
    EmbeddedWorkerStatus status;
  };

  void RecordEvent(
      EventType type,
      EmbeddedWorkerStatus status = EmbeddedWorkerStatus::STOPPED) {
    EventLog log = {type, status};
    events_.push_back(log);
  }

  void OnProcessAllocated() override { RecordEvent(PROCESS_ALLOCATED); }
  void OnStartWorkerMessageSent() override {
    RecordEvent(START_WORKER_MESSAGE_SENT);
  }
  void OnStarted() override { RecordEvent(STARTED); }
  void OnStopped(EmbeddedWorkerStatus old_status) override {
    RecordEvent(STOPPED, old_status);
  }
  void OnDetached(EmbeddedWorkerStatus old_status) override {
    RecordEvent(DETACHED, old_status);
  }

  bool OnMessageReceived(const IPC::Message& message) override { return false; }

  void SetUp() override {
    helper_.reset(new EmbeddedWorkerTestHelper(base::FilePath()));
  }

  void TearDown() override { helper_.reset(); }

  ServiceWorkerStatusCode StartWorker(EmbeddedWorkerInstance* worker,
                                      int id, const GURL& pattern,
                                      const GURL& url) {
    ServiceWorkerStatusCode status;
    base::RunLoop run_loop;
    std::unique_ptr<EmbeddedWorkerStartParams> params =
        CreateStartParams(id, pattern, url);
    worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                                run_loop.QuitClosure()));
    run_loop.Run();
    return status;
  }

  ServiceWorkerContextCore* context() { return helper_->context(); }

  EmbeddedWorkerRegistry* embedded_worker_registry() {
    DCHECK(context());
    return context()->embedded_worker_registry();
  }

  IPC::TestSink* ipc_sink() { return helper_->ipc_sink(); }

  std::vector<std::unique_ptr<
      EmbeddedWorkerTestHelper::MockEmbeddedWorkerInstanceClient>>*
  mock_instance_clients() {
    return helper_->mock_instance_clients();
  }

  TestBrowserThreadBundle thread_bundle_;
  std::unique_ptr<EmbeddedWorkerTestHelper> helper_;
  std::vector<EventLog> events_;

 private:
  DISALLOW_COPY_AND_ASSIGN(EmbeddedWorkerInstanceTest);
};

class EmbeddedWorkerInstanceTestP
    : public MojoServiceWorkerTestP<EmbeddedWorkerInstanceTest> {};

// A helper to simulate the start worker sequence is stalled in a worker
// process.
class StalledInStartWorkerHelper : public EmbeddedWorkerTestHelper {
 public:
  StalledInStartWorkerHelper() : EmbeddedWorkerTestHelper(base::FilePath()) {}
  ~StalledInStartWorkerHelper() override{};

  void OnStartWorker(int embedded_worker_id,
                     int64_t service_worker_version_id,
                     const GURL& scope,
                     const GURL& script_url,
                     bool pause_after_download) override {
    if (force_stall_in_start_) {
      // Do nothing to simulate a stall in the worker process.
      return;
    }
    EmbeddedWorkerTestHelper::OnStartWorker(embedded_worker_id,
                                            service_worker_version_id, scope,
                                            script_url, pause_after_download);
  }

  void set_force_stall_in_start(bool force_stall_in_start) {
    force_stall_in_start_ = force_stall_in_start;
  }

 private:
  bool force_stall_in_start_ = true;
};

class FailToSendIPCHelper : public EmbeddedWorkerTestHelper {
 public:
  FailToSendIPCHelper() : EmbeddedWorkerTestHelper(base::FilePath()) {}
  ~FailToSendIPCHelper() override {}

  bool Send(IPC::Message* message) override {
    delete message;
    return false;
  }
};

TEST_P(EmbeddedWorkerInstanceTestP, StartAndStop) {
  std::unique_ptr<EmbeddedWorkerInstance> worker =
      embedded_worker_registry()->CreateWorker();
  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());
  worker->AddListener(this);

  const int64_t service_worker_version_id = 55L;
  const GURL pattern("http://example.com/");
  const GURL url("http://example.com/worker.js");

  // Simulate adding one process to the pattern.
  helper_->SimulateAddProcessToPattern(pattern,
                                       helper_->mock_render_process_id());

  // Start should succeed.
  ServiceWorkerStatusCode status;
  base::RunLoop run_loop;
  std::unique_ptr<EmbeddedWorkerStartParams> params =
      CreateStartParams(service_worker_version_id, pattern, url);
  worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                              run_loop.QuitClosure()));
  EXPECT_EQ(EmbeddedWorkerStatus::STARTING, worker->status());
  run_loop.Run();
  EXPECT_EQ(SERVICE_WORKER_OK, status);

  // The 'WorkerStarted' message should have been sent by
  // EmbeddedWorkerTestHelper.
  EXPECT_EQ(EmbeddedWorkerStatus::RUNNING, worker->status());
  EXPECT_EQ(helper_->mock_render_process_id(), worker->process_id());

  // Stop the worker.
  EXPECT_EQ(SERVICE_WORKER_OK, worker->Stop());
  EXPECT_EQ(EmbeddedWorkerStatus::STOPPING, worker->status());
  base::RunLoop().RunUntilIdle();

  // The 'WorkerStopped' message should have been sent by
  // EmbeddedWorkerTestHelper.
  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());

  if (!is_mojo_enabled()) {
    // Verify that we've sent two messages via chromium IPC to start and
    // terminate the worker.
    ASSERT_TRUE(ipc_sink()->GetUniqueMessageMatching(
        EmbeddedWorkerMsg_StartWorker::ID));
    ASSERT_TRUE(
        ipc_sink()->GetUniqueMessageMatching(EmbeddedWorkerMsg_StopWorker::ID));
  }

  // Check if the IPCs are fired in expected order.
  ASSERT_EQ(4u, events_.size());
  EXPECT_EQ(PROCESS_ALLOCATED, events_[0].type);
  EXPECT_EQ(START_WORKER_MESSAGE_SENT, events_[1].type);
  EXPECT_EQ(STARTED, events_[2].type);
  EXPECT_EQ(STOPPED, events_[3].type);
}

// Test that a worker that failed twice will use a new render process
// on the next attempt.
TEST_P(EmbeddedWorkerInstanceTestP, ForceNewProcess) {
  std::unique_ptr<EmbeddedWorkerInstance> worker =
      embedded_worker_registry()->CreateWorker();
  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());

  const int64_t service_worker_version_id = 55L;
  const GURL pattern("http://example.com/");
  const GURL url("http://example.com/worker.js");

  // Simulate adding one process to the pattern.
  helper_->SimulateAddProcessToPattern(pattern,
                                       helper_->mock_render_process_id());

  // Also simulate adding a "newly created" process to the pattern because
  // unittests can't actually create a new process itself.
  // ServiceWorkerProcessManager only chooses this process id in unittests if
  // can_use_existing_process is false.
  helper_->SimulateAddProcessToPattern(pattern,
                                       helper_->new_render_process_id());

  {
    // Start once normally.
    ServiceWorkerStatusCode status = SERVICE_WORKER_ERROR_MAX_VALUE;
    base::RunLoop run_loop;
    std::unique_ptr<EmbeddedWorkerStartParams> params(
        CreateStartParams(service_worker_version_id, pattern, url));
    worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                                run_loop.QuitClosure()));
    run_loop.Run();
    EXPECT_EQ(SERVICE_WORKER_OK, status);
    EXPECT_EQ(EmbeddedWorkerStatus::RUNNING, worker->status());
    // The worker should be using the default render process.
    EXPECT_EQ(helper_->mock_render_process_id(), worker->process_id());

    EXPECT_EQ(SERVICE_WORKER_OK, worker->Stop());
    base::RunLoop().RunUntilIdle();
  }

  // Fail twice.
  context()->UpdateVersionFailureCount(service_worker_version_id,
                                       SERVICE_WORKER_ERROR_FAILED);
  context()->UpdateVersionFailureCount(service_worker_version_id,
                                       SERVICE_WORKER_ERROR_FAILED);

  {
    // Start again.
    ServiceWorkerStatusCode status;
    base::RunLoop run_loop;
    std::unique_ptr<EmbeddedWorkerStartParams> params(
        CreateStartParams(service_worker_version_id, pattern, url));
    worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                                run_loop.QuitClosure()));
    EXPECT_EQ(EmbeddedWorkerStatus::STARTING, worker->status());
    run_loop.Run();
    EXPECT_EQ(SERVICE_WORKER_OK, status);

    EXPECT_EQ(EmbeddedWorkerStatus::RUNNING, worker->status());
    // The worker should be using the new render process.
    EXPECT_EQ(helper_->new_render_process_id(), worker->process_id());
    EXPECT_EQ(SERVICE_WORKER_OK, worker->Stop());
    base::RunLoop().RunUntilIdle();
  }
}

TEST_P(EmbeddedWorkerInstanceTestP, StopWhenDevToolsAttached) {
  std::unique_ptr<EmbeddedWorkerInstance> worker =
      embedded_worker_registry()->CreateWorker();
  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());

  const int64_t service_worker_version_id = 55L;
  const GURL pattern("http://example.com/");
  const GURL url("http://example.com/worker.js");

  // Simulate adding one process to the pattern.
  helper_->SimulateAddProcessToPattern(pattern,
                                       helper_->mock_render_process_id());

  // Start the worker and then call StopIfIdle().
  EXPECT_EQ(SERVICE_WORKER_OK,
            StartWorker(worker.get(), service_worker_version_id, pattern, url));
  EXPECT_EQ(EmbeddedWorkerStatus::RUNNING, worker->status());
  EXPECT_EQ(helper_->mock_render_process_id(), worker->process_id());
  worker->StopIfIdle();
  EXPECT_EQ(EmbeddedWorkerStatus::STOPPING, worker->status());
  base::RunLoop().RunUntilIdle();

  // The worker must be stopped now.
  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());

  // Set devtools_attached to true, and do the same.
  worker->set_devtools_attached(true);

  EXPECT_EQ(SERVICE_WORKER_OK,
            StartWorker(worker.get(), service_worker_version_id, pattern, url));
  EXPECT_EQ(EmbeddedWorkerStatus::RUNNING, worker->status());
  EXPECT_EQ(helper_->mock_render_process_id(), worker->process_id());
  worker->StopIfIdle();
  base::RunLoop().RunUntilIdle();

  // The worker must not be stopped this time.
  EXPECT_EQ(EmbeddedWorkerStatus::RUNNING, worker->status());

  // Calling Stop() actually stops the worker regardless of whether devtools
  // is attached or not.
  EXPECT_EQ(SERVICE_WORKER_OK, worker->Stop());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());
}

// Test that the removal of a worker from the registry doesn't remove
// other workers in the same process.
TEST_P(EmbeddedWorkerInstanceTestP, RemoveWorkerInSharedProcess) {
  std::unique_ptr<EmbeddedWorkerInstance> worker1 =
      embedded_worker_registry()->CreateWorker();
  std::unique_ptr<EmbeddedWorkerInstance> worker2 =
      embedded_worker_registry()->CreateWorker();

  const int64_t version_id1 = 55L;
  const int64_t version_id2 = 56L;
  const GURL pattern("http://example.com/");
  const GURL url("http://example.com/worker.js");
  int process_id = helper_->mock_render_process_id();

  helper_->SimulateAddProcessToPattern(pattern, process_id);
  {
    // Start worker1.
    ServiceWorkerStatusCode status;
    base::RunLoop run_loop;
    std::unique_ptr<EmbeddedWorkerStartParams> params(
        CreateStartParams(version_id1, pattern, url));
    worker1->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                                 run_loop.QuitClosure()));
    run_loop.Run();
    EXPECT_EQ(SERVICE_WORKER_OK, status);
  }

  {
    // Start worker2.
    ServiceWorkerStatusCode status;
    base::RunLoop run_loop;
    std::unique_ptr<EmbeddedWorkerStartParams> params(
        CreateStartParams(version_id2, pattern, url));
    worker2->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                                 run_loop.QuitClosure()));
    run_loop.Run();
    EXPECT_EQ(SERVICE_WORKER_OK, status);
  }

  // The two workers share the same process.
  EXPECT_EQ(worker1->process_id(), worker2->process_id());

  // Destroy worker1. It removes itself from the registry.
  int worker1_id = worker1->embedded_worker_id();
  worker1->Stop();
  worker1.reset();

  // Only worker1 should be removed from the registry's process_map.
  EmbeddedWorkerRegistry* registry =
      helper_->context()->embedded_worker_registry();
  EXPECT_EQ(0UL, registry->worker_process_map_[process_id].count(worker1_id));
  EXPECT_EQ(1UL, registry->worker_process_map_[process_id].count(
                     worker2->embedded_worker_id()));

  worker2->Stop();
}

TEST_P(EmbeddedWorkerInstanceTestP, DetachDuringProcessAllocation) {
  const int64_t version_id = 55L;
  const GURL scope("http://example.com/");
  const GURL url("http://example.com/worker.js");

  std::unique_ptr<EmbeddedWorkerInstance> worker =
      embedded_worker_registry()->CreateWorker();
  worker->AddListener(this);

  // Run the start worker sequence and detach during process allocation.
  ServiceWorkerStatusCode status = SERVICE_WORKER_ERROR_MAX_VALUE;
  std::unique_ptr<EmbeddedWorkerStartParams> params(
      CreateStartParams(version_id, scope, url));
  worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                              base::Bind(&base::DoNothing)));
  worker->Detach();
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());
  EXPECT_EQ(ChildProcessHost::kInvalidUniqueID, worker->process_id());

  // The start callback should not be aborted by detach (see a comment on the
  // dtor of EmbeddedWorkerInstance::StartTask).
  EXPECT_EQ(SERVICE_WORKER_ERROR_MAX_VALUE, status);

  // "PROCESS_ALLOCATED" event should not be recorded.
  ASSERT_EQ(1u, events_.size());
  EXPECT_EQ(DETACHED, events_[0].type);
  EXPECT_EQ(EmbeddedWorkerStatus::STARTING, events_[0].status);
}

TEST_P(EmbeddedWorkerInstanceTestP, DetachAfterSendingStartWorkerMessage) {
  const int64_t version_id = 55L;
  const GURL scope("http://example.com/");
  const GURL url("http://example.com/worker.js");

  helper_.reset(new StalledInStartWorkerHelper());
  std::unique_ptr<EmbeddedWorkerInstance> worker =
      embedded_worker_registry()->CreateWorker();
  worker->AddListener(this);

  // Run the start worker sequence until a start worker message is sent.
  ServiceWorkerStatusCode status = SERVICE_WORKER_ERROR_MAX_VALUE;
  std::unique_ptr<EmbeddedWorkerStartParams> params(
      CreateStartParams(version_id, scope, url));
  worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                              base::Bind(&base::DoNothing)));
  base::RunLoop().RunUntilIdle();

  ASSERT_EQ(2u, events_.size());
  EXPECT_EQ(PROCESS_ALLOCATED, events_[0].type);
  EXPECT_EQ(START_WORKER_MESSAGE_SENT, events_[1].type);
  events_.clear();

  worker->Detach();
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());
  EXPECT_EQ(ChildProcessHost::kInvalidUniqueID, worker->process_id());

  // The start callback should not be aborted by detach (see a comment on the
  // dtor of EmbeddedWorkerInstance::StartTask).
  EXPECT_EQ(SERVICE_WORKER_ERROR_MAX_VALUE, status);

  // "STARTED" event should not be recorded.
  ASSERT_EQ(1u, events_.size());
  EXPECT_EQ(DETACHED, events_[0].type);
  EXPECT_EQ(EmbeddedWorkerStatus::STARTING, events_[0].status);
}

TEST_P(EmbeddedWorkerInstanceTestP, StopDuringProcessAllocation) {
  const int64_t version_id = 55L;
  const GURL scope("http://example.com/");
  const GURL url("http://example.com/worker.js");

  std::unique_ptr<EmbeddedWorkerInstance> worker =
      embedded_worker_registry()->CreateWorker();
  worker->AddListener(this);

  // Stop the start worker sequence before a process is allocated.
  ServiceWorkerStatusCode status = SERVICE_WORKER_ERROR_MAX_VALUE;

  std::unique_ptr<EmbeddedWorkerStartParams> params(
      CreateStartParams(version_id, scope, url));
  worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                              base::Bind(&base::DoNothing)));
  worker->Stop();
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());
  EXPECT_EQ(ChildProcessHost::kInvalidUniqueID, worker->process_id());

  // The start callback should not be aborted by stop (see a comment on the dtor
  // of EmbeddedWorkerInstance::StartTask).
  EXPECT_EQ(SERVICE_WORKER_ERROR_MAX_VALUE, status);

  // "PROCESS_ALLOCATED" event should not be recorded.
  ASSERT_EQ(1u, events_.size());
  EXPECT_EQ(DETACHED, events_[0].type);
  if (is_mojo_enabled()) {
    // STOPPING should be recorded here because EmbeddedWorkerInstance must be
    // detached while executing RunUntilIdle.
    EXPECT_EQ(EmbeddedWorkerStatus::STOPPING, events_[0].status);
  } else {
    EXPECT_EQ(EmbeddedWorkerStatus::STARTING, events_[0].status);
  }
  events_.clear();

  // Restart the worker.
  status = SERVICE_WORKER_ERROR_MAX_VALUE;
  std::unique_ptr<base::RunLoop> run_loop(new base::RunLoop);
  params = CreateStartParams(version_id, scope, url);
  worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                              run_loop->QuitClosure()));
  run_loop->Run();

  EXPECT_EQ(SERVICE_WORKER_OK, status);
  ASSERT_EQ(3u, events_.size());
  EXPECT_EQ(PROCESS_ALLOCATED, events_[0].type);
  EXPECT_EQ(START_WORKER_MESSAGE_SENT, events_[1].type);
  EXPECT_EQ(STARTED, events_[2].type);

  // Tear down the worker.
  worker->Stop();
}

TEST_P(EmbeddedWorkerInstanceTestP, StopDuringPausedAfterDownload) {
  const int64_t version_id = 55L;
  const GURL scope("http://example.com/");
  const GURL url("http://example.com/worker.js");

  std::unique_ptr<EmbeddedWorkerInstance> worker =
      embedded_worker_registry()->CreateWorker();
  worker->AddListener(this);

  // Run the start worker sequence until pause after download.
  ServiceWorkerStatusCode status = SERVICE_WORKER_ERROR_MAX_VALUE;

  std::unique_ptr<EmbeddedWorkerStartParams> params(
      CreateStartParams(version_id, scope, url));
  params->pause_after_download = true;
  worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                              base::Bind(&base::DoNothing)));
  base::RunLoop().RunUntilIdle();

  // Make the worker stopping and attempt to send a resume after download
  // message.
  worker->Stop();
  worker->ResumeAfterDownload();
  base::RunLoop().RunUntilIdle();

  // The resume after download message should not have been sent.
  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());
  EXPECT_FALSE(ipc_sink()->GetFirstMessageMatching(
      EmbeddedWorkerMsg_ResumeAfterDownload::ID));
}

TEST_P(EmbeddedWorkerInstanceTestP, StopAfterSendingStartWorkerMessage) {
  const int64_t version_id = 55L;
  const GURL scope("http://example.com/");
  const GURL url("http://example.com/worker.js");

  helper_.reset(new StalledInStartWorkerHelper);
  std::unique_ptr<EmbeddedWorkerInstance> worker =
      embedded_worker_registry()->CreateWorker();
  worker->AddListener(this);

  // Run the start worker sequence until a start worker message is sent.
  ServiceWorkerStatusCode status = SERVICE_WORKER_ERROR_MAX_VALUE;
  std::unique_ptr<EmbeddedWorkerStartParams> params(
      CreateStartParams(version_id, scope, url));
  worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                              base::Bind(&base::DoNothing)));
  base::RunLoop().RunUntilIdle();

  ASSERT_EQ(2u, events_.size());
  EXPECT_EQ(PROCESS_ALLOCATED, events_[0].type);
  EXPECT_EQ(START_WORKER_MESSAGE_SENT, events_[1].type);
  events_.clear();

  worker->Stop();
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());
  EXPECT_EQ(ChildProcessHost::kInvalidUniqueID, worker->process_id());

  // The start callback should not be aborted by stop (see a comment on the dtor
  // of EmbeddedWorkerInstance::StartTask).
  EXPECT_EQ(SERVICE_WORKER_ERROR_MAX_VALUE, status);

  // "STARTED" event should not be recorded.
  ASSERT_EQ(1u, events_.size());
  EXPECT_EQ(STOPPED, events_[0].type);
  EXPECT_EQ(EmbeddedWorkerStatus::STOPPING, events_[0].status);
  events_.clear();

  // Restart the worker.
  static_cast<StalledInStartWorkerHelper*>(helper_.get())
      ->set_force_stall_in_start(false);
  status = SERVICE_WORKER_ERROR_MAX_VALUE;
  std::unique_ptr<base::RunLoop> run_loop(new base::RunLoop);

  params = CreateStartParams(version_id, scope, url);
  worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                              run_loop->QuitClosure()));
  run_loop->Run();

  // The worker should be started.
  EXPECT_EQ(SERVICE_WORKER_OK, status);
  ASSERT_EQ(3u, events_.size());
  EXPECT_EQ(PROCESS_ALLOCATED, events_[0].type);
  EXPECT_EQ(START_WORKER_MESSAGE_SENT, events_[1].type);
  EXPECT_EQ(STARTED, events_[2].type);

  // Tear down the worker.
  worker->Stop();
}

TEST_P(EmbeddedWorkerInstanceTestP, Detach) {
  const int64_t version_id = 55L;
  const GURL pattern("http://example.com/");
  const GURL url("http://example.com/worker.js");
  std::unique_ptr<EmbeddedWorkerInstance> worker =
      embedded_worker_registry()->CreateWorker();
  helper_->SimulateAddProcessToPattern(pattern,
                                       helper_->mock_render_process_id());
  ServiceWorkerStatusCode status = SERVICE_WORKER_ERROR_FAILED;
  worker->AddListener(this);

  // Start the worker.
  base::RunLoop run_loop;
  std::unique_ptr<EmbeddedWorkerStartParams> params(
      CreateStartParams(version_id, pattern, url));
  worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                              run_loop.QuitClosure()));
  run_loop.Run();

  // Detach.
  int process_id = worker->process_id();
  worker->Detach();
  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());

  // Send the registry a message from the detached worker. Nothing should
  // happen.
  embedded_worker_registry()->OnWorkerStarted(process_id,
                                              worker->embedded_worker_id());
  EXPECT_EQ(EmbeddedWorkerStatus::STOPPED, worker->status());
}

// Test for when sending the start IPC failed.
TEST_P(EmbeddedWorkerInstanceTestP, FailToSendStartIPC) {
  const int64_t version_id = 55L;
  const GURL pattern("http://example.com/");
  const GURL url("http://example.com/worker.js");

  if (is_mojo_enabled()) {
    // Let StartWorker fail; mojo IPC fails to connect to a remote interface.
    helper_->RegisterMockInstanceClient(nullptr);
  } else {
    helper_.reset(new FailToSendIPCHelper());
  }

  std::unique_ptr<EmbeddedWorkerInstance> worker =
      embedded_worker_registry()->CreateWorker();
  helper_->SimulateAddProcessToPattern(pattern,
                                       helper_->mock_render_process_id());
  ServiceWorkerStatusCode status = SERVICE_WORKER_ERROR_FAILED;
  worker->AddListener(this);

  // Attempt to start the worker.
  std::unique_ptr<EmbeddedWorkerStartParams> params(
      CreateStartParams(version_id, pattern, url));
  if (is_mojo_enabled()) {
    worker->Start(std::move(params),
                  base::Bind(&ServiceWorkerUtils::NoOpStatusCallback));
    base::RunLoop().RunUntilIdle();
  } else {
    base::RunLoop run_loop;
    worker->Start(std::move(params), base::Bind(&SaveStatusAndCall, &status,
                                                run_loop.QuitClosure()));
    run_loop.Run();
  }

  if (is_mojo_enabled()) {
    // Worker should handle the failure of binding as detach.
    ASSERT_EQ(3u, events_.size());
    EXPECT_EQ(PROCESS_ALLOCATED, events_[0].type);
    EXPECT_EQ(START_WORKER_MESSAGE_SENT, events_[1].type);
    EXPECT_EQ(DETACHED, events_[2].type);
    EXPECT_EQ(EmbeddedWorkerStatus::STARTING, events_[2].status);
  } else {
    // The callback should have run, and we should have got an OnStopped
    // message.
    EXPECT_EQ(SERVICE_WORKER_ERROR_IPC_FAILED, status);
    ASSERT_EQ(2u, events_.size());
    EXPECT_EQ(PROCESS_ALLOCATED, events_[0].type);
    EXPECT_EQ(STOPPED, events_[1].type);
    EXPECT_EQ(EmbeddedWorkerStatus::STARTING, events_[1].status);
  }
}

class FailEmbeddedWorkerInstanceClientImpl
    : public EmbeddedWorkerTestHelper::MockEmbeddedWorkerInstanceClient {
 public:
  explicit FailEmbeddedWorkerInstanceClientImpl(
      base::WeakPtr<EmbeddedWorkerTestHelper> helper)
      : EmbeddedWorkerTestHelper::MockEmbeddedWorkerInstanceClient(helper) {}

 private:
  void StartWorker(
      const EmbeddedWorkerStartParams& /* unused */,
      service_manager::mojom::InterfaceProviderPtr /* unused */,
      service_manager::mojom::InterfaceProviderRequest /* unused */) override {
    helper_->mock_instance_clients()->clear();
  }
};

TEST_P(EmbeddedWorkerInstanceTestP, RemoveRemoteInterface) {
  if (!is_mojo_enabled())
    return;

  const int64_t version_id = 55L;
  const GURL pattern("http://example.com/");
  const GURL url("http://example.com/worker.js");

  // Let StartWorker fail; binding is discarded in the middle of IPC
  helper_->RegisterMockInstanceClient(
      base::MakeUnique<FailEmbeddedWorkerInstanceClientImpl>(
          helper_->AsWeakPtr()));
  ASSERT_EQ(mock_instance_clients()->size(), 1UL);

  std::unique_ptr<EmbeddedWorkerInstance> worker =
      embedded_worker_registry()->CreateWorker();
  helper_->SimulateAddProcessToPattern(pattern,
                                       helper_->mock_render_process_id());
  worker->AddListener(this);

  // Attempt to start the worker.
  std::unique_ptr<EmbeddedWorkerStartParams> params(
      CreateStartParams(version_id, pattern, url));
  worker->Start(std::move(params),
                base::Bind(&ServiceWorkerUtils::NoOpStatusCallback));
  base::RunLoop().RunUntilIdle();

  // Worker should handle the sudden shutdown as detach.
  ASSERT_EQ(3u, events_.size());
  EXPECT_EQ(PROCESS_ALLOCATED, events_[0].type);
  EXPECT_EQ(START_WORKER_MESSAGE_SENT, events_[1].type);
  EXPECT_EQ(DETACHED, events_[2].type);
  EXPECT_EQ(EmbeddedWorkerStatus::STARTING, events_[2].status);
}

INSTANTIATE_TEST_CASE_P(EmbeddedWorkerInstanceTest,
                        EmbeddedWorkerInstanceTestP,
                        ::testing::Values(false, true));

}  // namespace content
