chromium/ash/components/arc/timer/arc_timer_bridge_unittest.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/components/arc/timer/arc_timer_bridge.h"

#include <map>
#include <memory>
#include <optional>
#include <utility>
#include <vector>

#include "ash/components/arc/mojom/timer.mojom.h"
#include "ash/components/arc/session/arc_bridge_service.h"
#include "ash/components/arc/session/arc_service_manager.h"
#include "ash/components/arc/session/connection_holder.h"
#include "ash/components/arc/test/connection_holder_util.h"
#include "ash/components/arc/test/fake_timer_instance.h"
#include "ash/components/arc/timer/arc_timer_mojom_traits.h"
#include "base/files/file_descriptor_watcher_posix.h"
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/posix/unix_domain_socket.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chromeos/ash/components/dbus/upstart/fake_upstart_client.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
#include "components/user_prefs/test/test_browser_context_with_prefs.h"
#include "content/public/test/browser_task_environment.h"
#include "mojo/public/cpp/system/handle.h"
#include "mojo/public/cpp/system/platform_handle.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace arc {

namespace {

// Converts a system file descriptor to a mojo handle that can be sent to the
// host.
mojo::ScopedHandle WrapPlatformFd(base::ScopedFD scoped_fd) {
  mojo::ScopedHandle handle = mojo::WrapPlatformFile(std::move(scoped_fd));
  if (!handle.is_valid()) {
    LOG(ERROR) << "Failed to wrap platform handle";
    return mojo::ScopedHandle();
  }
  return handle;
}

// Callback for D-Bus operations.
void TimerOperationCallback(base::OnceClosure quit_callback,
                            bool* op_result,
                            mojom::ArcTimerResult result) {
  *op_result = (result == mojom::ArcTimerResult::SUCCESS);
  std::move(quit_callback).Run();
}

// Stores clock ids and their corresponding file descriptors. These file
// descriptors indicate when a timer corresponding to the clock has expired on
// a read.
class ArcTimerStore {
 public:
  ArcTimerStore() = default;

  ArcTimerStore(const ArcTimerStore&) = delete;
  ArcTimerStore& operator=(const ArcTimerStore&) = delete;

  bool AddTimer(clockid_t clock_id, base::ScopedFD read_fd) {
    return arc_timers_.emplace(clock_id, std::move(read_fd)).second;
  }

  void ClearTimers() { return arc_timers_.clear(); }

  std::optional<int> GetTimerReadFd(clockid_t clock_id) {
    if (!HasTimer(clock_id))
      return std::nullopt;
    return std::optional<int>(arc_timers_[clock_id].get());
  }

  bool HasTimer(clockid_t clock_id) const {
    auto it = arc_timers_.find(clock_id);
    return it != arc_timers_.end() && it->second.is_valid();
  }

 private:
  // Map of a clock id to read fd that is signalled when the timer corresponding
  // the clock expires.
  std::map<clockid_t, base::ScopedFD> arc_timers_;
};

class ArcTimerTest : public testing::Test {
 public:
  ArcTimerTest()
      : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP) {
    ash::UpstartClient::InitializeFake();
    chromeos::PowerManagerClient::InitializeFake();
    timer_bridge_ = ArcTimerBridge::GetForBrowserContextForTesting(&context_);
    // This results in ArcTimerBridge::OnInstanceReady being called.
    ArcServiceManager::Get()->arc_bridge_service()->timer()->SetInstance(
        &timer_instance_);
    WaitForInstanceReady(
        ArcServiceManager::Get()->arc_bridge_service()->timer());
  }

  ArcTimerTest(const ArcTimerTest&) = delete;
  ArcTimerTest& operator=(const ArcTimerTest&) = delete;

  ~ArcTimerTest() override {
    // Destroys the FakeTimerInstance. This results in
    // ArcTimerBridge::OnInstanceClosed being called.
    ArcServiceManager::Get()->arc_bridge_service()->timer()->CloseInstance(
        &timer_instance_);
    timer_bridge_->Shutdown();
    chromeos::PowerManagerClient::Shutdown();
    ash::UpstartClient::Shutdown();
  }

 protected:
  // Returns true iff timer creation of each clock type succeeded.
  bool CreateTimers(const std::vector<clockid_t>& clocks);

  // Returns true iff a timer for |clock_id| is successfully scheduled.
  bool StartTimer(clockid_t clock_id, base::TimeTicks absolute_expiration_time);

  // Returns true iff the read descriptor of a timer is signalled. If the
  // signalling is incorrect returns false. Blocks otherwise.
  bool WaitForExpiration(clockid_t clock_id);

  mojom::TimerHost* GetTimerHost() { return timer_instance_.GetTimerHost(); }

 private:
  // Stores |read_fds| corresponding to clock ids in |clocks| in
  // |arc_timer_store_|.
  bool StoreReadFds(const std::vector<clockid_t> clocks,
                    std::vector<base::ScopedFD> read_fds);

  content::BrowserTaskEnvironment task_environment_;
  ArcServiceManager arc_service_manager_;
  user_prefs::TestBrowserContextWithPrefs context_;
  FakeTimerInstance timer_instance_;

  ArcTimerStore arc_timer_store_;

  raw_ptr<ArcTimerBridge> timer_bridge_;
};

bool ArcTimerTest::StoreReadFds(const std::vector<clockid_t> clocks,
                                std::vector<base::ScopedFD> read_fds) {
  auto read_fd_iter = read_fds.begin();
  for (auto clock_id : clocks) {
    // This should never fail because at this point timers have been created by
    // powerd and |clocks| doesn't have any duplicate clock ids.
    if (!arc_timer_store_.AddTimer(clock_id, std::move(*read_fd_iter))) {
      LOG(ERROR) << "Error while adding clock=" << clock_id << " to store";
      arc_timer_store_.ClearTimers();
      return false;
    }
    read_fd_iter++;
  }
  return true;
}

bool ArcTimerTest::CreateTimers(const std::vector<clockid_t>& clocks) {
  // Create requests to create a timer for each clock.
  std::vector<mojom::CreateTimerRequestPtr> arc_timer_requests;
  std::vector<base::ScopedFD> read_fds;
  for (auto clock_id : clocks) {
    mojom::CreateTimerRequestPtr request = mojom::CreateTimerRequest::New();
    // Create a socket pair for each clock. One socket will be part of the
    // mojo argument and will be used by the host to indicate when the timer
    // expires. The other socket will be used to detect the expiration of the
    // timer by epolling and reading.
    base::ScopedFD read_fd;
    base::ScopedFD write_fd;
    if (!base::CreateSocketPair(&read_fd, &write_fd)) {
      LOG(ERROR) << "Failed to create socket pair for ARC timers";
      return false;
    }
    request->clock_id = clock_id;
    request->expiration_fd = WrapPlatformFd(std::move(write_fd));
    arc_timer_requests.emplace_back(std::move(request));

    read_fds.emplace_back(std::move(read_fd));
  }

  // Clear local test state before creating timers.
  arc_timer_store_.ClearTimers();

  // Call the host to create timers. Safe to use base::Unretained(this) as the
  // class is guaranteed to exist for the duration of the test.
  bool result;
  base::RunLoop loop;

  timer_instance_.GetTimerHost()->CreateTimers(
      std::move(arc_timer_requests),
      base::BindOnce(&TimerOperationCallback, loop.QuitClosure(), &result));
  loop.Run();
  if (!result)
    return false;

  // If timer creation succeeded, store the read fds associated with each clock
  // in the store. The read fd will be used to wait on for a timer expiration.
  if (!StoreReadFds(clocks, std::move(read_fds))) {
    return false;
  }

  return true;
}

bool ArcTimerTest::StartTimer(clockid_t clock_id,
                              base::TimeTicks absolute_expiration_time) {
  // Call the host to start a timer corresponding to |clock_id|. Safe to use
  // base::Unretained(this) as the class is guaranteed to exist for the
  // duration of the test.
  base::RunLoop loop;
  bool result;
  timer_instance_.GetTimerHost()->StartTimer(
      clock_id, absolute_expiration_time,
      base::BindOnce(&TimerOperationCallback, loop.QuitClosure(), &result));
  loop.Run();
  return result;
}

bool ArcTimerTest::WaitForExpiration(clockid_t clock_id) {
  if (!arc_timer_store_.HasTimer(clock_id)) {
    LOG(ERROR) << "Timer of clock=" << clock_id << " not present";
    return false;
  }

  // Wait for the host to indicate expiration by watching the read end of the
  // socket pair.
  std::optional<int> timer_read_fd_opt =
      arc_timer_store_.GetTimerReadFd(clock_id);
  // This should never happen if the timer was present in the store.
  if (!timer_read_fd_opt.has_value()) {
    ADD_FAILURE() << "Clock=" << clock_id << " read fd not found";
    return false;
  }
  int timer_read_fd = timer_read_fd_opt.value();
  base::RunLoop loop;
  std::unique_ptr<base::FileDescriptorWatcher::Controller>
      watch_readable_controller = base::FileDescriptorWatcher::WatchReadable(
          timer_read_fd, loop.QuitClosure());
  loop.Run();

  // The timer expects 8 bytes to be written from the host upon expiration and
  // the number of expirations to be 1.
  uint64_t num_expirations;
  std::vector<base::ScopedFD> fds;
  ssize_t bytes_read = base::UnixDomainSocket::RecvMsg(
      timer_read_fd, &num_expirations, sizeof(num_expirations), &fds);
  if (bytes_read < static_cast<ssize_t>(sizeof(num_expirations))) {
    LOG(ERROR) << "Incorrect timer wake up bytes_read=" << bytes_read;
    return false;
  }
  EXPECT_EQ(num_expirations, 1ULL);

  // TODO(fdoray): Remove this hack once WatchReadable fixes crbug.com/74118.
  // This is required for |watch_readable_controller| to clean up properly.
  base::RunLoop run_loop;
  run_loop.RunUntilIdle();
  return true;
}

TEST_F(ArcTimerTest, StartTimerTest) {
  std::vector<clockid_t> clocks = {CLOCK_REALTIME_ALARM, CLOCK_BOOTTIME_ALARM};
  // Create timers before starting it.
  EXPECT_TRUE(CreateTimers(clocks));
  // Start timer and check if timer expired.
  base::TimeDelta delay = base::Milliseconds(20);
  EXPECT_TRUE(StartTimer(CLOCK_BOOTTIME_ALARM, base::TimeTicks::Now() + delay));
  EXPECT_TRUE(WaitForExpiration(CLOCK_BOOTTIME_ALARM));
}

TEST_F(ArcTimerTest, InvalidCreateTimersArgsTest) {
  std::vector<clockid_t> clocks = {CLOCK_REALTIME_ALARM, CLOCK_BOOTTIME_ALARM,
                                   CLOCK_BOOTTIME_ALARM};
  // Timers with duplicate clock ids shouldn't succeed.
  EXPECT_FALSE(CreateTimers(clocks));
}

TEST_F(ArcTimerTest, InvalidStartTimerArgsTest) {
  std::vector<clockid_t> clocks = {CLOCK_REALTIME_ALARM};
  EXPECT_TRUE(CreateTimers(clocks));
  // Start timer should fail due to un-registered clock id.
  base::TimeDelta delay = base::Milliseconds(20);
  EXPECT_FALSE(
      StartTimer(CLOCK_BOOTTIME_ALARM, base::TimeTicks::Now() + delay));
}

TEST_F(ArcTimerTest, CheckMultipleCreateTimersTest) {
  std::vector<clockid_t> clocks = {CLOCK_REALTIME_ALARM};
  EXPECT_TRUE(CreateTimers(clocks));
  // The power manager implicitly deletes old timers associated with a tag
  // during a create call. Thus, consecutive create calls should succeed.
  EXPECT_TRUE(CreateTimers(clocks));
}

TEST_F(ArcTimerTest, SetTimeTest_RequestedTimeIsInvalid) {
  // Time::Now() + 25 hours should be rejected.
  base::Time time_to_set =
      base::Time::Now() + kArcSetTimeMaxTimeDelta + base::Hours(1);
  base::test::TestFuture<mojom::ArcTimerResult> future;
  GetTimerHost()->SetTime(time_to_set, future.GetCallback());
  EXPECT_EQ(future.Get(), mojom::ArcTimerResult::FAILURE);

  // Time::Now() - 25 hours should be rejected.
  time_to_set = base::Time::Now() - kArcSetTimeMaxTimeDelta - base::Hours(1);
  base::test::TestFuture<mojom::ArcTimerResult> future2;
  GetTimerHost()->SetTime(time_to_set, future2.GetCallback());
  EXPECT_EQ(future2.Get(), mojom::ArcTimerResult::FAILURE);
}

TEST_F(ArcTimerTest, SetTimeTest_RequestedTimeIsValid) {
  // Time::Now() + 23 hours should be accepted.
  const base::Time time_to_set =
      base::Time::Now() + kArcSetTimeMaxTimeDelta - base::Hours(1);

  ash::FakeUpstartClient::Get()->set_start_job_cb(base::BindLambdaForTesting(
      [time_to_set](const std::string& job_name,
                    const std::vector<std::string>& env) {
        EXPECT_EQ(job_name, kArcSetTimeJobName);
        EXPECT_EQ(env.size(), 1U);  // Can't use ASSERT_EQ inside the closure.
        if (env.size() >= 1) {
          EXPECT_EQ(env[0], base::StringPrintf("UNIXTIME_TO_SET=%ld",
                                               time_to_set.ToTimeT()));
        }
        return ash::FakeUpstartClient::StartJobResult(true /* success */);
      }));

  base::test::TestFuture<mojom::ArcTimerResult> future;
  GetTimerHost()->SetTime(time_to_set, future.GetCallback());
  EXPECT_EQ(future.Get(), mojom::ArcTimerResult::SUCCESS);
}

TEST_F(ArcTimerTest, SetTimeTest_UpstartJobFails) {
  const base::Time time_to_set =
      base::Time::Now() + kArcSetTimeMaxTimeDelta - base::Hours(1);

  ash::FakeUpstartClient::Get()->set_start_job_cb(base::BindRepeating(
      [](const std::string& job_name, const std::vector<std::string>& env) {
        // Upstart job fails.
        return ash::FakeUpstartClient::StartJobResult(false /* success */);
      }));

  base::test::TestFuture<mojom::ArcTimerResult> future;
  GetTimerHost()->SetTime(time_to_set, future.GetCallback());
  EXPECT_EQ(future.Get(), mojom::ArcTimerResult::FAILURE);
}

}  // namespace

}  // namespace arc