blob: 899852c378a4415fd19b81306edf6ab5af9c5bc8 [file] [log] [blame]
// Copyright 2018 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 "device/gamepad/hid_haptic_gamepad_base.h"
#include <memory>
#include "base/bind.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/test/scoped_task_environment.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 HidHapticGamepadBase::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::TimeDelta::FromMillisecondsD(kDurationMillis);
// An implementation of HidHapticGamepadBase that records its output reports.
class FakeHidHapticGamepad : public HidHapticGamepadBase {
public:
FakeHidHapticGamepad(const HidHapticGamepadBase::HapticReportData& data)
: HidHapticGamepadBase(data) {}
~FakeHidHapticGamepad() override = default;
size_t WriteOutputReport(void* report, size_t report_length) override {
std::vector<uint8_t> report_bytes(report_length);
const uint8_t* report_begin = reinterpret_cast<uint8_t*>(report);
const uint8_t* report_end = report_begin + report_length;
std::copy(report_begin, report_end, report_bytes.begin());
output_reports_.push_back(std::move(report_bytes));
return report_length;
}
std::vector<std::vector<uint8_t>> output_reports_;
};
// Main test fixture
class HidHapticGamepadBaseTest : public testing::Test {
public:
HidHapticGamepadBaseTest()
: start_vibration_output_report_(kStartVibrationData,
kStartVibrationData + kReportLength),
stop_vibration_output_report_(kStopVibrationData,
kStopVibrationData + kReportLength),
first_callback_count_(0),
second_callback_count_(0),
first_callback_result_(
mojom::GamepadHapticsResult::GamepadHapticsResultError),
second_callback_result_(
mojom::GamepadHapticsResult::GamepadHapticsResultError),
gamepad_(std::make_unique<FakeHidHapticGamepad>(kHapticReportData)) {}
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),
std::move(callback), base::ThreadTaskRunnerHandle::Get());
}
void PostResetVibration(
mojom::GamepadHapticsManager::ResetVibrationActuatorCallback callback) {
gamepad_->ResetVibration(std::move(callback),
base::ThreadTaskRunnerHandle::Get());
}
// 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_;
std::unique_ptr<FakeHidHapticGamepad> gamepad_;
base::test::ScopedTaskEnvironment scoped_task_environment_{
base::test::ScopedTaskEnvironment::MainThreadType::MOCK_TIME};
DISALLOW_COPY_AND_ASSIGN(HidHapticGamepadBaseTest);
};
TEST_F(HidHapticGamepadBaseTest, PlayEffectTest) {
EXPECT_TRUE(gamepad_->output_reports_.empty());
EXPECT_EQ(0, first_callback_count_);
PostPlayEffect(kZeroStartDelayMillis, kStrongMagnitude, kWeakMagnitude,
base::BindOnce(&HidHapticGamepadBaseTest::FirstCallback,
base::Unretained(this)));
// Run the queued task and start vibration.
scoped_task_environment_.RunUntilIdle();
EXPECT_THAT(gamepad_->output_reports_,
testing::ElementsAre(start_vibration_output_report_));
EXPECT_EQ(0, first_callback_count_);
EXPECT_GT(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
// Finish the effect.
scoped_task_environment_.FastForwardBy(kPendingTaskDuration);
EXPECT_THAT(gamepad_->output_reports_,
testing::ElementsAre(start_vibration_output_report_));
EXPECT_EQ(1, first_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
first_callback_result_);
EXPECT_EQ(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
}
TEST_F(HidHapticGamepadBaseTest, ResetVibrationTest) {
EXPECT_TRUE(gamepad_->output_reports_.empty());
EXPECT_EQ(0, first_callback_count_);
PostResetVibration(base::BindOnce(&HidHapticGamepadBaseTest::FirstCallback,
base::Unretained(this)));
// Run the queued task and reset vibration.
scoped_task_environment_.RunUntilIdle();
EXPECT_THAT(gamepad_->output_reports_,
testing::ElementsAre(stop_vibration_output_report_));
EXPECT_EQ(1, first_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
first_callback_result_);
EXPECT_EQ(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
}
TEST_F(HidHapticGamepadBaseTest, ZeroVibrationTest) {
EXPECT_TRUE(gamepad_->output_reports_.empty());
EXPECT_EQ(0, first_callback_count_);
PostPlayEffect(kZeroStartDelayMillis, kZeroMagnitude, kZeroMagnitude,
base::BindOnce(&HidHapticGamepadBaseTest::FirstCallback,
base::Unretained(this)));
// Run the queued task.
scoped_task_environment_.RunUntilIdle();
EXPECT_THAT(gamepad_->output_reports_,
testing::ElementsAre(stop_vibration_output_report_));
EXPECT_EQ(0, first_callback_count_);
EXPECT_GT(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
// Finish the effect.
scoped_task_environment_.FastForwardBy(kPendingTaskDuration);
EXPECT_THAT(gamepad_->output_reports_,
testing::ElementsAre(stop_vibration_output_report_));
EXPECT_EQ(1, first_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
first_callback_result_);
EXPECT_EQ(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
}
TEST_F(HidHapticGamepadBaseTest, StartDelayTest) {
EXPECT_TRUE(gamepad_->output_reports_.empty());
EXPECT_EQ(0, first_callback_count_);
// Issue PlayEffect with non-zero |start_delay|.
PostPlayEffect(kNonZeroStartDelayMillis, kStrongMagnitude, kWeakMagnitude,
base::BindOnce(&HidHapticGamepadBaseTest::FirstCallback,
base::Unretained(this)));
// Stop vibration for the delay period.
scoped_task_environment_.RunUntilIdle();
EXPECT_THAT(gamepad_->output_reports_,
testing::ElementsAre(stop_vibration_output_report_));
EXPECT_EQ(0, first_callback_count_);
EXPECT_GT(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
// Start vibration.
scoped_task_environment_.FastForwardBy(kPendingTaskDuration);
EXPECT_THAT(gamepad_->output_reports_,
testing::ElementsAre(stop_vibration_output_report_,
start_vibration_output_report_));
EXPECT_EQ(0, first_callback_count_);
EXPECT_GT(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
// Finish the effect.
scoped_task_environment_.FastForwardBy(kPendingTaskDuration);
EXPECT_THAT(gamepad_->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(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
}
TEST_F(HidHapticGamepadBaseTest, ZeroStartDelayPreemptionTest) {
EXPECT_TRUE(gamepad_->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(&HidHapticGamepadBaseTest::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(&HidHapticGamepadBaseTest::SecondCallback,
base::Unretained(this)));
// Execute the pending tasks.
scoped_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(gamepad_->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(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
// Finish the effect.
scoped_task_environment_.FastForwardBy(kPendingTaskDuration);
EXPECT_THAT(gamepad_->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(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
}
TEST_F(HidHapticGamepadBaseTest, NonZeroStartDelayPreemptionTest) {
EXPECT_TRUE(gamepad_->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(&HidHapticGamepadBaseTest::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(&HidHapticGamepadBaseTest::SecondCallback,
base::Unretained(this)));
// Execute the pending tasks.
scoped_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(gamepad_->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(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
// Start vibration.
scoped_task_environment_.FastForwardBy(kPendingTaskDuration);
EXPECT_THAT(gamepad_->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(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
// Finish the effect.
scoped_task_environment_.FastForwardBy(kPendingTaskDuration);
EXPECT_THAT(gamepad_->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(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
}
TEST_F(HidHapticGamepadBaseTest, ResetVibrationPreemptionTest) {
EXPECT_TRUE(gamepad_->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(&HidHapticGamepadBaseTest::FirstCallback,
base::Unretained(this)));
// Reset vibration. This should cause the effect to be preempted before it
// calls SetVibration.
PostResetVibration(base::BindOnce(&HidHapticGamepadBaseTest::SecondCallback,
base::Unretained(this)));
// Execute the pending tasks.
scoped_task_environment_.RunUntilIdle();
EXPECT_THAT(gamepad_->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(scoped_task_environment_.GetPendingMainThreadTaskCount(), 0u);
}
} // namespace
} // namespace device