chromium/ash/webui/diagnostics_ui/backend/input/input_data_event_watcher_unittest.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "ash/webui/diagnostics_ui/backend/input/input_data_event_watcher.h"

#include <fcntl.h>
#include <linux/input-event-codes.h>
#include <linux/input.h>

#include <cstdint>
#include <memory>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/posix/eintr_wrapper.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash::diagnostics {

namespace {

constexpr uint32_t kFakeEvdevId = 999;
constexpr char kFakeEvdevPath[] = "/dev/input/event999";
constexpr timeval kFakeTimeval = {.tv_sec = 1493076832, .tv_usec = 526871};

// Sample of events that an InputDataEventWatcher might process.
constexpr struct input_event kFakeKeyCaplPressAndRelease[] = {
    // Begin press on "left alt"
    {kFakeTimeval, EV_MSC, MSC_SCAN, 0x0},
    {kFakeTimeval, EV_KEY, KEY_LEFTALT, 1},
    {kFakeTimeval, EV_SYN, SYN_REPORT, 1},
    // Begin press on "left meta"
    {kFakeTimeval, EV_MSC, MSC_SCAN, 0x38},
    {kFakeTimeval, EV_KEY, KEY_LEFTMETA, 1},
    {kFakeTimeval, EV_SYN, SYN_REPORT, 1},
    // Release press on "left meta"
    {kFakeTimeval, EV_MSC, MSC_SCAN, 0xdb},
    {kFakeTimeval, EV_KEY, KEY_LEFTMETA, 0},
    {kFakeTimeval, EV_SYN, SYN_REPORT, 1},
    // Release press on "left alt"
    {kFakeTimeval, EV_MSC, MSC_SCAN, 0x38},
    {kFakeTimeval, EV_KEY, KEY_LEFTALT, 0},
    {kFakeTimeval, EV_SYN, SYN_REPORT, 1},
    // Enable LED
    {kFakeTimeval, EV_LED, LED_CAPSL, 1},
    {kFakeTimeval, EV_SYN, SYN_REPORT, 1}};

class FakeWatcher : public InputDataEventWatcher {
 public:
  FakeWatcher(uint32_t evdev_id, const base::FilePath& path, int fd)
      : InputDataEventWatcher(evdev_id, path, fd) {}
  ~FakeWatcher() override = default;

  void SetQuitClosure(base::OnceClosure quit_closure) {
    quit_closure_ = std::move(quit_closure);
  }

  void ProcessEvent(const input_event& event) override {
    events.emplace_back(event);
    if (quit_closure_) {
      std::move(quit_closure_).Run();
    }
  }

  // Test helpers:
  size_t num_events_processed() { return events.size(); }
  const input_event GetCall(int index) { return events[index]; }

 private:
  std::vector<input_event> events;
  base::OnceClosure quit_closure_;
};

class InputDataEventWatcherTest : public testing::Test {
 public:
  InputDataEventWatcherTest()
      : task_environment_(
            std::make_unique<base::test::SingleThreadTaskEnvironment>(
                base::test::SingleThreadTaskEnvironment::MainThreadType::UI)),
        test_path_(base::FilePath(kFakeEvdevPath)) {}
  InputDataEventWatcherTest(const InputDataEventWatcherTest&) = delete;
  InputDataEventWatcherTest& operator=(const InputDataEventWatcherTest&) =
      delete;
  ~InputDataEventWatcherTest() override = default;

  void SetUp() override { SetupWatcher(); }

  void TearDown() override { TearDownWatcher(); }

  // Write event to write endpoint of pipe created during setup.
  void WriteInputEvent(const input_event& input) {
    int write_result = write(writefd(), &input, sizeof(input));
    if (write_result < 0)
      PLOG(WARNING) << "write";
    ASSERT_TRUE(write_result >= 0);
  }

  FakeWatcher* watcher() { return watcher_.get(); }
  int readfd() { return pipefds_[0]; }
  int writefd() { return pipefds_[1]; }

  // Check expected event data matches event "processed" by FakeWatcher.
  void VerifyInputEvent(const input_event& expected, int call_index) {
    EXPECT_EQ(call_index + 1ul, watcher()->num_events_processed());
    auto written_event = watcher()->GetCall(call_index);
    EXPECT_EQ(expected.type, written_event.type);
    EXPECT_EQ(expected.code, written_event.code);
    EXPECT_EQ(expected.value, written_event.value);
  }

  base::test::SingleThreadTaskEnvironment* task_environment() {
    return task_environment_.get();
  }

 protected:
  void SetupWatcher() {
    // Create pipe to fake having a `/dev/input/event{evdev_id}` file.
    EXPECT_TRUE(base::CreateLocalNonBlockingPipe(pipefds_));
    watcher_ =
        std::make_unique<FakeWatcher>(kFakeEvdevId, test_path_, readfd());
  }

  void TearDownWatcher() {
    watcher()->Stop();
    watcher_.reset();

    // Only close the write endpoint. InputDataEventWatcher::Stop will close
    // down the read endpoint. Calling close on the read will cause BADF.
    if (IGNORE_EINTR(close(writefd())) < 0)
      PLOG(WARNING) << "close (writefd)";
  }

 private:
  std::unique_ptr<base::test::SingleThreadTaskEnvironment> task_environment_;
  int pipefds_[2];
  const base::FilePath test_path_;
  std::unique_ptr<FakeWatcher> watcher_;
};

// Verify that valid input_event can be read and
// trigger`InputDataEventWatcher::ProcessEvent`.
TEST_F(InputDataEventWatcherTest, ReadsInputEventsFromFd) {
  watcher()->Start();
  size_t write_count = 0ul;
  for (const auto& event : kFakeKeyCaplPressAndRelease) {
    base::RunLoop run_loop;
    // Wait for read to trigger `InputDataEventWatcher::ProcessEvent`.
    watcher()->SetQuitClosure(
        base::BindLambdaForTesting([&]() { run_loop.Quit(); }));
    WriteInputEvent(event);
    run_loop.Run();
    // Ensure expected event is passed to ProcessEvent.
    VerifyInputEvent(event, write_count);
    write_count++;
  }
}

}  // namespace

}  // namespace ash::diagnostics