910e62b5创建于 1月15日历史提交
// 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 "device/gamepad/hid_haptic_gamepad.h"

#include <memory>

#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/task_environment.h"
#include "device/gamepad/hid_writer.h"
#include "device/gamepad/public/mojom/gamepad.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace device {

namespace {

// Describes a hypothetical HID haptic gamepad with two 16-bit vibration
// magnitude fields.
constexpr uint8_t kReportId = 0x42;
constexpr size_t kReportLength = 5;
constexpr HidHapticGamepad::HapticReportData kHapticReportData = {
    0x1234, 0xabcd, kReportId, kReportLength, 1, 3, 16, 0, 0xffff};

// The expected "stop vibration" report bytes (zero vibration).
constexpr uint8_t kStopVibrationData[] = {kReportId, 0x00, 0x00, 0x00, 0x00};
// The expected "start vibration" report bytes for an effect with 100% intensity
// on the "strong" vibration channel and 50% intensity on the "weak" vibration
// channel.
constexpr uint8_t kStartVibrationData[] = {kReportId, 0xff, 0xff, 0xff, 0x7f};

// Use 1 ms for all non-zero effect durations. There is no reason to test longer
// delays as they will be skipped anyway.
constexpr double kDurationMillis = 1.0;
constexpr double kNonZeroStartDelayMillis = 1.0;
// Setting |start_delay| to zero can cause additional reports to be sent.
constexpr double kZeroStartDelayMillis = 0.0;
// Vibration magnitudes for the strong and weak channels of a typical
// dual-rumble vibration effect. kStartVibrationData describes a report with
// these magnitudes.
constexpr double kStrongMagnitude = 1.0;  // 100% intensity
constexpr double kWeakMagnitude = 0.5;    // 50% intensity
// Zero vibration magnitude. kStopVibrationData describes a report with zero
// vibration on both channels.
constexpr double kZeroMagnitude = 0.0;

constexpr base::TimeDelta kPendingTaskDuration =
    base::Milliseconds(kDurationMillis);

class FakeHidWriter : public HidWriter {
 public:
  FakeHidWriter() = default;
  ~FakeHidWriter() override = default;

  // HidWriter implementation.
  size_t WriteOutputReport(base::span<const uint8_t> report) override {
    output_reports.emplace_back(report.begin(), report.end());
    return report.size_bytes();
  }

  std::vector<std::vector<uint8_t>> output_reports;
};

// Main test fixture
class HidHapticGamepadTest : public testing::Test {
 public:
  HidHapticGamepadTest()
      : start_vibration_output_report_(base::ToVector(kStartVibrationData)),
        stop_vibration_output_report_(base::ToVector(kStopVibrationData)),
        first_callback_count_(0),
        second_callback_count_(0),
        first_callback_result_(
            mojom::GamepadHapticsResult::GamepadHapticsResultError),
        second_callback_result_(
            mojom::GamepadHapticsResult::GamepadHapticsResultError) {
    auto fake_hid_writer = std::make_unique<FakeHidWriter>();
    fake_hid_writer_ = fake_hid_writer.get();
    gamepad_ = std::make_unique<HidHapticGamepad>(kHapticReportData,
                                                  std::move(fake_hid_writer));
  }

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

  void TearDown() override { gamepad_->Shutdown(); }

  void PostPlayEffect(
      double start_delay,
      double strong_magnitude,
      double weak_magnitude,
      mojom::GamepadHapticsManager::PlayVibrationEffectOnceCallback callback) {
    gamepad_->PlayEffect(
        mojom::GamepadHapticEffectType::GamepadHapticEffectTypeDualRumble,
        mojom::GamepadEffectParameters::New(
            kDurationMillis, start_delay, strong_magnitude, weak_magnitude,
            /*left_trigger=*/0, /*right_trigger=*/0),
        std::move(callback), base::SingleThreadTaskRunner::GetCurrentDefault());
  }

  void PostResetVibration(
      mojom::GamepadHapticsManager::ResetVibrationActuatorCallback callback) {
    gamepad_->ResetVibration(std::move(callback),
                             base::SingleThreadTaskRunner::GetCurrentDefault());
  }

  // Callback for the first PlayEffect or ResetVibration call in a test.
  void FirstCallback(mojom::GamepadHapticsResult result) {
    first_callback_count_++;
    first_callback_result_ = result;
  }

  // Callback for the second PlayEffect or ResetVibration call in a test. Use
  // this when multiple callbacks may be received and the test should check the
  // result codes for each.
  void SecondCallback(mojom::GamepadHapticsResult result) {
    second_callback_count_++;
    second_callback_result_ = result;
  }

  const std::vector<uint8_t> start_vibration_output_report_;
  const std::vector<uint8_t> stop_vibration_output_report_;
  int first_callback_count_;
  int second_callback_count_;
  mojom::GamepadHapticsResult first_callback_result_;
  mojom::GamepadHapticsResult second_callback_result_;
  raw_ptr<FakeHidWriter, DanglingUntriaged> fake_hid_writer_;
  std::unique_ptr<HidHapticGamepad> gamepad_;
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
};

TEST_F(HidHapticGamepadTest, PlayEffectTest) {
  EXPECT_TRUE(fake_hid_writer_->output_reports.empty());
  EXPECT_EQ(0, first_callback_count_);

  PostPlayEffect(kZeroStartDelayMillis, kStrongMagnitude, kWeakMagnitude,
                 base::BindOnce(&HidHapticGamepadTest::FirstCallback,
                                base::Unretained(this)));

  // Run the queued task and start vibration.
  task_environment_.RunUntilIdle();

  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(start_vibration_output_report_));
  EXPECT_EQ(0, first_callback_count_);
  EXPECT_GT(task_environment_.GetPendingMainThreadTaskCount(), 0u);

  // Finish the effect.
  task_environment_.FastForwardBy(kPendingTaskDuration);

  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(start_vibration_output_report_));
  EXPECT_EQ(1, first_callback_count_);
  EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
            first_callback_result_);
  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
}

TEST_F(HidHapticGamepadTest, ResetVibrationTest) {
  EXPECT_TRUE(fake_hid_writer_->output_reports.empty());
  EXPECT_EQ(0, first_callback_count_);

  PostResetVibration(base::BindOnce(&HidHapticGamepadTest::FirstCallback,
                                    base::Unretained(this)));

  // Run the queued task and reset vibration.
  task_environment_.RunUntilIdle();

  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(stop_vibration_output_report_));
  EXPECT_EQ(1, first_callback_count_);
  EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
            first_callback_result_);
  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
}

TEST_F(HidHapticGamepadTest, ZeroVibrationTest) {
  EXPECT_TRUE(fake_hid_writer_->output_reports.empty());
  EXPECT_EQ(0, first_callback_count_);

  PostPlayEffect(kZeroStartDelayMillis, kZeroMagnitude, kZeroMagnitude,
                 base::BindOnce(&HidHapticGamepadTest::FirstCallback,
                                base::Unretained(this)));

  // Run the queued task.
  task_environment_.RunUntilIdle();

  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(stop_vibration_output_report_));
  EXPECT_EQ(0, first_callback_count_);
  EXPECT_GT(task_environment_.GetPendingMainThreadTaskCount(), 0u);

  // Finish the effect.
  task_environment_.FastForwardBy(kPendingTaskDuration);

  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(stop_vibration_output_report_));
  EXPECT_EQ(1, first_callback_count_);
  EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
            first_callback_result_);
  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
}

TEST_F(HidHapticGamepadTest, StartDelayTest) {
  EXPECT_TRUE(fake_hid_writer_->output_reports.empty());
  EXPECT_EQ(0, first_callback_count_);

  // Issue PlayEffect with non-zero |start_delay|.
  PostPlayEffect(kNonZeroStartDelayMillis, kStrongMagnitude, kWeakMagnitude,
                 base::BindOnce(&HidHapticGamepadTest::FirstCallback,
                                base::Unretained(this)));

  // Stop vibration for the delay period.
  task_environment_.RunUntilIdle();

  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(stop_vibration_output_report_));
  EXPECT_EQ(0, first_callback_count_);
  EXPECT_GT(task_environment_.GetPendingMainThreadTaskCount(), 0u);

  // Start vibration.
  task_environment_.FastForwardBy(kPendingTaskDuration);

  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(stop_vibration_output_report_,
                                   start_vibration_output_report_));
  EXPECT_EQ(0, first_callback_count_);
  EXPECT_GT(task_environment_.GetPendingMainThreadTaskCount(), 0u);

  // Finish the effect.
  task_environment_.FastForwardBy(kPendingTaskDuration);

  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(stop_vibration_output_report_,
                                   start_vibration_output_report_));
  EXPECT_EQ(1, first_callback_count_);
  EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
            first_callback_result_);
  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
}

TEST_F(HidHapticGamepadTest, ZeroStartDelayPreemptionTest) {
  EXPECT_TRUE(fake_hid_writer_->output_reports.empty());
  EXPECT_EQ(0, first_callback_count_);
  EXPECT_EQ(0, second_callback_count_);

  // Start an ongoing effect. We'll preempt this one with another effect.
  PostPlayEffect(kZeroStartDelayMillis, kStrongMagnitude, kWeakMagnitude,
                 base::BindOnce(&HidHapticGamepadTest::FirstCallback,
                                base::Unretained(this)));

  // Start a second effect with zero |start_delay|. This should cause the first
  // effect to be preempted before it calls SetVibration.
  PostPlayEffect(kZeroStartDelayMillis, kStrongMagnitude, kWeakMagnitude,
                 base::BindOnce(&HidHapticGamepadTest::SecondCallback,
                                base::Unretained(this)));

  // Execute the pending tasks.
  task_environment_.RunUntilIdle();

  // The first effect should have already returned with a "preempted" result
  // without issuing a report. The second effect has started vibration.
  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(start_vibration_output_report_));
  EXPECT_EQ(1, first_callback_count_);
  EXPECT_EQ(0, second_callback_count_);
  EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultPreempted,
            first_callback_result_);
  EXPECT_GT(task_environment_.GetPendingMainThreadTaskCount(), 0u);

  // Finish the effect.
  task_environment_.FastForwardBy(kPendingTaskDuration);

  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(start_vibration_output_report_));
  EXPECT_EQ(1, first_callback_count_);
  EXPECT_EQ(1, second_callback_count_);
  EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
            second_callback_result_);
  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
}

TEST_F(HidHapticGamepadTest, NonZeroStartDelayPreemptionTest) {
  EXPECT_TRUE(fake_hid_writer_->output_reports.empty());
  EXPECT_EQ(0, first_callback_count_);
  EXPECT_EQ(0, second_callback_count_);

  // Start an ongoing effect. We'll preempt this one with another effect.
  PostPlayEffect(kZeroStartDelayMillis, kStrongMagnitude, kWeakMagnitude,
                 base::BindOnce(&HidHapticGamepadTest::FirstCallback,
                                base::Unretained(this)));

  // Start a second effect with non-zero |start_delay|. This should cause the
  // first effect to be preempted before it calls SetVibration.
  PostPlayEffect(kNonZeroStartDelayMillis, kStrongMagnitude, kWeakMagnitude,
                 base::BindOnce(&HidHapticGamepadTest::SecondCallback,
                                base::Unretained(this)));

  // Execute the pending tasks.
  task_environment_.RunUntilIdle();

  // The first effect should have already returned with a "preempted" result.
  // Because the second effect has a non-zero |start_delay| and is preempting
  // another effect, it will call SetZeroVibration to ensure no vibration
  // occurs during its |start_delay| period.
  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(stop_vibration_output_report_));
  EXPECT_EQ(1, first_callback_count_);
  EXPECT_EQ(0, second_callback_count_);
  EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultPreempted,
            first_callback_result_);
  EXPECT_GT(task_environment_.GetPendingMainThreadTaskCount(), 0u);

  // Start vibration.
  task_environment_.FastForwardBy(kPendingTaskDuration);

  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(stop_vibration_output_report_,
                                   start_vibration_output_report_));
  EXPECT_EQ(1, first_callback_count_);
  EXPECT_EQ(0, second_callback_count_);
  EXPECT_GT(task_environment_.GetPendingMainThreadTaskCount(), 0u);

  // Finish the effect.
  task_environment_.FastForwardBy(kPendingTaskDuration);

  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(stop_vibration_output_report_,
                                   start_vibration_output_report_));
  EXPECT_EQ(1, first_callback_count_);
  EXPECT_EQ(1, second_callback_count_);
  EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
            second_callback_result_);
  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
}

TEST_F(HidHapticGamepadTest, ResetVibrationPreemptionTest) {
  EXPECT_TRUE(fake_hid_writer_->output_reports.empty());
  EXPECT_EQ(0, first_callback_count_);
  EXPECT_EQ(0, second_callback_count_);

  // Start an ongoing effect. We'll preempt it with a reset.
  PostPlayEffect(kZeroStartDelayMillis, kStrongMagnitude, kWeakMagnitude,
                 base::BindOnce(&HidHapticGamepadTest::FirstCallback,
                                base::Unretained(this)));

  // Reset vibration. This should cause the effect to be preempted before it
  // calls SetVibration.
  PostResetVibration(base::BindOnce(&HidHapticGamepadTest::SecondCallback,
                                    base::Unretained(this)));

  // Execute the pending tasks.
  task_environment_.RunUntilIdle();

  EXPECT_THAT(fake_hid_writer_->output_reports,
              testing::ElementsAre(stop_vibration_output_report_));
  EXPECT_EQ(1, first_callback_count_);
  EXPECT_EQ(1, second_callback_count_);
  EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultPreempted,
            first_callback_result_);
  EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
            second_callback_result_);
  EXPECT_EQ(task_environment_.GetPendingMainThreadTaskCount(), 0u);
}

}  // namespace

}  // namespace device