| // Copyright 2020 The Fuchsia 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 <lib/fpromise/single_threaded_executor.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "lib/inspect/cpp/hierarchy.h" |
| #include "lib/inspect/cpp/inspector.h" |
| #include "lib/inspect/cpp/reader.h" |
| #include "src/lib/fxl/strings/join_strings.h" |
| #include "src/lib/testing/loop_fixture/real_loop_fixture.h" |
| #include "src/lib/testing/loop_fixture/test_loop_fixture.h" |
| #include "src/ui/scenic/lib/input/touch_injector.h" |
| #include "src/ui/scenic/lib/utils/math.h" |
| |
| using Phase = fuchsia::ui::pointerinjector::EventPhase; |
| using fuchsia::ui::pointerinjector::DevicePtr; |
| using fuchsia::ui::pointerinjector::DeviceType; |
| using InjectionEvent = fuchsia::ui::pointerinjector::Event; |
| using StreamId = scenic_impl::input::StreamId; |
| |
| // Unit tests for the Injector class. |
| |
| namespace input::test { |
| |
| namespace { |
| |
| // clang-format off |
| static constexpr std::array<float, 9> kIdentityMatrix = { |
| 1, 0, 0, // first column |
| 0, 1, 0, // second column |
| 0, 0, 1, // third column |
| }; |
| // clang-format on |
| |
| scenic_impl::input::InjectorSettings InjectorSettingsTemplate() { |
| return {.dispatch_policy = fuchsia::ui::pointerinjector::DispatchPolicy::EXCLUSIVE_TARGET, |
| .device_id = 1, |
| .device_type = DeviceType::TOUCH, |
| .context_koid = 1, |
| .target_koid = 2}; |
| } |
| |
| scenic_impl::input::Viewport ViewportTemplate() { |
| return { |
| .extents = std::array<std::array<float, 2>, 2>{{{0, 0}, {1000, 1000}}}, |
| .context_from_viewport_transform = utils::ColumnMajorMat3ArrayToMat4(kIdentityMatrix), |
| }; |
| } |
| |
| InjectionEvent InjectionEventTemplate() { |
| InjectionEvent event; |
| event.set_timestamp(1111); |
| { |
| fuchsia::ui::pointerinjector::PointerSample pointer_sample; |
| pointer_sample.set_pointer_id(2222); |
| pointer_sample.set_phase(Phase::CHANGE); |
| pointer_sample.set_position_in_viewport({333, 444}); |
| fuchsia::ui::pointerinjector::Data data; |
| data.set_pointer_sample(std::move(pointer_sample)); |
| event.set_data(std::move(data)); |
| } |
| return event; |
| } |
| |
| } // namespace |
| |
| TEST(InjectorTest, InjectedEvents_ShouldTriggerTheInjectLambda) { |
| async::TestLoop test_loop; |
| |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool error_callback_fired = false; |
| injector.set_error_handler([&error_callback_fired](zx_status_t) { error_callback_fired = true; }); |
| |
| bool connectivity_is_good = true; |
| uint32_t num_injections = 0; |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/ |
| [&connectivity_is_good](zx_koid_t, zx_koid_t) { return connectivity_is_good; }, |
| /*inject=*/[&num_injections](auto...) { ++num_injections; }, |
| /*on_channel_closed=*/[] {}); |
| |
| { // Inject one event. |
| bool injection_callback_fired = false; |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::ADD); |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| test_loop.RunUntilIdle(); |
| EXPECT_TRUE(injection_callback_fired); |
| } |
| |
| EXPECT_EQ(num_injections, 1u); |
| |
| { // Inject CHANGE event. |
| bool injection_callback_fired = false; |
| std::vector<InjectionEvent> events; |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::CHANGE); |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| test_loop.RunUntilIdle(); |
| EXPECT_TRUE(injection_callback_fired); |
| |
| EXPECT_EQ(num_injections, 2u); |
| } |
| |
| { // Inject remove event. |
| bool injection_callback_fired = false; |
| std::vector<InjectionEvent> events; |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::REMOVE); |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| test_loop.RunUntilIdle(); |
| EXPECT_TRUE(injection_callback_fired); |
| } |
| |
| EXPECT_EQ(num_injections, 3u); |
| EXPECT_FALSE(error_callback_fired); |
| } |
| |
| TEST(InjectorTest, InjectionWithNoEvent_ShouldCloseChannel) { |
| // Test loop to be able to control dispatch without having to create an entire test class |
| // subclassing TestLoopFixture. |
| async::TestLoop test_loop; |
| |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool error_callback_fired = false; |
| injector.set_error_handler([&error_callback_fired](zx_status_t) { error_callback_fired = true; }); |
| |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/ |
| [](auto...) { return true; }, |
| /*inject=*/ |
| [](auto...) {}, |
| /*on_channel_closed=*/[] {}); |
| |
| bool injection_callback_fired = false; |
| // Inject nothing. |
| injector->Inject({}, [&injection_callback_fired] { injection_callback_fired = true; }); |
| test_loop.RunUntilIdle(); |
| |
| EXPECT_FALSE(injection_callback_fired); |
| EXPECT_TRUE(error_callback_fired); |
| } |
| |
| TEST(InjectorTest, ClientClosingChannel_ShouldTriggerCancelEvents_ForEachOngoingStream) { |
| // Test loop to be able to control dispatch without having to create an entire test class |
| // subclassing TestLoopFixture. |
| async::TestLoop test_loop; |
| |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool error_callback_fired = false; |
| injector.set_error_handler([&error_callback_fired](zx_status_t) { error_callback_fired = true; }); |
| |
| std::vector<uint32_t> cancelled_streams; |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/ |
| [](auto...) { return true; }, |
| /*inject=*/ |
| [&cancelled_streams](const scenic_impl::input::InternalTouchEvent& event, StreamId) { |
| if (event.phase == scenic_impl::input::Phase::kCancel) |
| cancelled_streams.push_back(event.pointer_id); |
| }, |
| /*on_channel_closed=*/[] {}); |
| |
| // Start three streams and end one. |
| { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_pointer_id(1); |
| event.mutable_data()->pointer_sample().set_phase(Phase::ADD); |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, [] {}); |
| } |
| { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_pointer_id(2); |
| event.mutable_data()->pointer_sample().set_phase(Phase::ADD); |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, [] {}); |
| } |
| { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_pointer_id(3); |
| event.mutable_data()->pointer_sample().set_phase(Phase::ADD); |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, [] {}); |
| } |
| { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_pointer_id(1); |
| event.mutable_data()->pointer_sample().set_phase(Phase::REMOVE); |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, [] {}); |
| } |
| |
| // Close the client side channel. |
| injector = {}; |
| test_loop.RunUntilIdle(); |
| |
| // Should receive two CANCEL events, since there should be two ongoing streams. |
| EXPECT_FALSE(error_callback_fired); |
| EXPECT_THAT(cancelled_streams, testing::UnorderedElementsAre(2, 3)); |
| } |
| |
| TEST(InjectorTest, ServerClosingChannel_ShouldTriggerCancelEvents_ForEachOngoingStream) { |
| // Test loop to be able to control dispatch without having to create an entire test class |
| // subclassing TestLoopFixture. |
| async::TestLoop test_loop; |
| |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool error_callback_fired = false; |
| injector.set_error_handler([&error_callback_fired](zx_status_t) { error_callback_fired = true; }); |
| |
| std::vector<uint32_t> cancelled_streams; |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/ |
| [](auto...) { return true; }, |
| /*inject=*/ |
| [&cancelled_streams](const scenic_impl::input::InternalTouchEvent& event, StreamId) { |
| if (event.phase == scenic_impl::input::Phase::kCancel) |
| cancelled_streams.push_back(event.pointer_id); |
| }, |
| /*on_channel_closed=*/[] {}); |
| |
| // Start three streams and end one. |
| { |
| std::vector<InjectionEvent> events; |
| { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_pointer_id(1); |
| event.mutable_data()->pointer_sample().set_phase(Phase::ADD); |
| events.emplace_back(std::move(event)); |
| } |
| { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_pointer_id(2); |
| event.mutable_data()->pointer_sample().set_phase(Phase::ADD); |
| events.emplace_back(std::move(event)); |
| } |
| { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_pointer_id(3); |
| event.mutable_data()->pointer_sample().set_phase(Phase::ADD); |
| events.emplace_back(std::move(event)); |
| } |
| { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_pointer_id(1); |
| event.mutable_data()->pointer_sample().set_phase(Phase::REMOVE); |
| events.emplace_back(std::move(event)); |
| } |
| injector->Inject({std::move(events)}, [] {}); |
| } |
| |
| // Inject an event with missing fields to cause the channel to close. |
| { |
| std::vector<InjectionEvent> events; |
| events.emplace_back(); |
| injector->Inject(std::move(events), [] {}); |
| } |
| test_loop.RunUntilIdle(); |
| |
| EXPECT_TRUE(error_callback_fired); |
| // Should receive CANCEL events for the two ongoing streams; 2 and 3. |
| EXPECT_THAT(cancelled_streams, testing::UnorderedElementsAre(2, 3)); |
| } |
| |
| TEST(InjectorTest, InjectionOfEmptyEvent_ShouldCloseChannel) { |
| // Test loop to be able to control dispatch without having to create an entire test class |
| // subclassing TestLoopFixture. |
| async::TestLoop test_loop; |
| |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool error_callback_fired = false; |
| injector.set_error_handler([&error_callback_fired](auto) { error_callback_fired = true; }); |
| |
| bool injection_lambda_fired = false; |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/ |
| [](zx_koid_t, zx_koid_t) { return true; }, |
| /*inject=*/ |
| [&injection_lambda_fired](auto...) { injection_lambda_fired = true; }, |
| /*on_channel_closed=*/[] {}); |
| |
| bool injection_callback_fired = false; |
| InjectionEvent event; |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| test_loop.RunUntilIdle(); |
| |
| EXPECT_FALSE(injection_lambda_fired); |
| EXPECT_FALSE(injection_callback_fired); |
| EXPECT_TRUE(error_callback_fired); |
| } |
| |
| TEST(InjectorTest, ClientClosingChannel_ShouldTriggerOnChannelClosedLambda) { |
| // Test loop to be able to control dispatch without having to create an entire test class |
| // subclassing TestLoopFixture. |
| async::TestLoop test_loop; |
| |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool client_error_callback_fired = false; |
| injector.set_error_handler( |
| [&client_error_callback_fired](zx_status_t) { client_error_callback_fired = true; }); |
| |
| bool on_channel_closed_callback_fired = false; |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/[](auto...) { return true; }, |
| /*inject=*/[](auto...) {}, |
| /*on_channel_closed=*/ |
| [&on_channel_closed_callback_fired] { on_channel_closed_callback_fired = true; }); |
| |
| // Close the client side channel. |
| injector = {}; |
| test_loop.RunUntilIdle(); |
| |
| EXPECT_FALSE(client_error_callback_fired); |
| EXPECT_TRUE(on_channel_closed_callback_fired); |
| } |
| |
| TEST(InjectorTest, ServerClosingChannel_ShouldTriggerOnChannelClosedLambda) { |
| // Test loop to be able to control dispatch without having to create an entire test class |
| // subclassing TestLoopFixture. |
| async::TestLoop test_loop; |
| |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool client_error_callback_fired = false; |
| injector.set_error_handler( |
| [&client_error_callback_fired](zx_status_t) { client_error_callback_fired = true; }); |
| |
| bool on_channel_closed_callback_fired = false; |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/[](auto...) { return true; }, |
| /*inject=*/[](auto...) {}, |
| /*on_channel_closed=*/ |
| [&on_channel_closed_callback_fired] { on_channel_closed_callback_fired = true; }); |
| |
| // Inject an event with missing fields to cause the channel to close. |
| { |
| std::vector<InjectionEvent> events; |
| events.emplace_back(); |
| injector->Inject(std::move(events), [] {}); |
| } |
| test_loop.RunUntilIdle(); |
| |
| EXPECT_TRUE(client_error_callback_fired); |
| EXPECT_TRUE(on_channel_closed_callback_fired); |
| } |
| |
| // Test for lazy connectivity detection. |
| TEST(InjectorTest, InjectionWithBadConnectivity_ShouldCloseChannel) { |
| // Test loop to be able to control dispatch without having to create an entire test class |
| // subclassing TestLoopFixture. |
| async::TestLoop test_loop; |
| |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool error_callback_fired = false; |
| zx_status_t error = ZX_OK; |
| injector.set_error_handler([&error_callback_fired, &error](zx_status_t status) { |
| error_callback_fired = true; |
| error = status; |
| }); |
| |
| bool connectivity_is_good = true; |
| uint32_t num_cancel_events = 0; |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/ |
| [&connectivity_is_good](zx_koid_t, zx_koid_t) { return connectivity_is_good; }, |
| /*inject=*/ |
| [&num_cancel_events](const scenic_impl::input::InternalTouchEvent& event, StreamId) { |
| num_cancel_events += event.phase == scenic_impl::input::Phase::kCancel ? 1 : 0; |
| }, |
| /*on_channel_closed=*/[] {}); |
| |
| // Start event stream while connectivity is good. |
| { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::ADD); |
| event.mutable_data()->pointer_sample().set_pointer_id(1); |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, [] {}); |
| test_loop.RunUntilIdle(); |
| } |
| |
| // Connectivity was good. No problems. |
| EXPECT_FALSE(error_callback_fired); |
| |
| // Inject with bad connectivity. |
| connectivity_is_good = false; |
| { |
| bool injection_callback_fired = false; |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::CHANGE); |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| test_loop.RunUntilIdle(); |
| EXPECT_FALSE(injection_callback_fired); |
| } |
| |
| // Connectivity was bad, so channel should be closed and an extra CANCEL event should have been |
| // injected for each ongoing stream. |
| EXPECT_EQ(num_cancel_events, 1u); |
| EXPECT_TRUE(error_callback_fired); |
| EXPECT_EQ(error, ZX_ERR_BAD_STATE); |
| } |
| |
| // Class for testing parameterized injection of invalid events. |
| // Takes an int that determines which field gets deleted (parameter must be copyable). |
| class InjectorInvalidEventsTest : public gtest::TestLoopFixture, |
| public testing::WithParamInterface<int> {}; |
| |
| INSTANTIATE_TEST_SUITE_P(InjectEventWithMissingField_ShouldCloseChannel, InjectorInvalidEventsTest, |
| testing::Range(0, 3)); |
| |
| TEST_P(InjectorInvalidEventsTest, InjectEventWithMissingField_ShouldCloseChannel) { |
| // Create event with a missing field based on GetParam(). |
| InjectionEvent event = InjectionEventTemplate(); |
| switch (GetParam()) { |
| case 0: |
| event.mutable_data()->pointer_sample().clear_pointer_id(); |
| break; |
| case 1: |
| event.mutable_data()->pointer_sample().clear_phase(); |
| break; |
| case 2: |
| event.mutable_data()->pointer_sample().clear_position_in_viewport(); |
| break; |
| } |
| |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool error_callback_fired = false; |
| zx_status_t error = ZX_OK; |
| injector.set_error_handler([&error_callback_fired, &error](zx_status_t status) { |
| error_callback_fired = true; |
| error = status; |
| }); |
| |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/ |
| [](auto...) { return true; }, |
| /*inject=*/ |
| [](auto...) {}, |
| /*on_channel_closed=*/[] {}); |
| |
| bool injection_callback_fired = false; |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| RunLoopUntilIdle(); |
| |
| EXPECT_FALSE(injection_callback_fired); |
| EXPECT_TRUE(error_callback_fired); |
| EXPECT_EQ(error, ZX_ERR_INVALID_ARGS); |
| } |
| |
| // Class for testing different event streams. |
| // Each invocation gets a vector of pairs of pointer ids and Phases, representing pointer streams. |
| class InjectorGoodEventStreamTest |
| : public gtest::TestLoopFixture, |
| public testing::WithParamInterface<std::vector<std::pair</*pointer_id*/ uint32_t, Phase>>> {}; |
| |
| static std::vector<std::vector<std::pair<uint32_t, Phase>>> GoodStreamTestData() { |
| // clang-format off |
| return { |
| {{1, Phase::ADD}, {1, Phase::REMOVE}}, // 0: one pointer trivial |
| {{1, Phase::ADD}, {1, Phase::CHANGE}, {1, Phase::REMOVE}}, // 1: one pointer minimal all phases |
| {{1, Phase::ADD}, {1, Phase::CANCEL}}, // 2: one pointer trivial cancelled |
| {{1, Phase::ADD}, {1, Phase::CHANGE}, {1, Phase::CANCEL}}, // 3: one pointer minimal all phases cancelled |
| {{1, Phase::ADD}, {1, Phase::CHANGE}, {1, Phase::CANCEL}, |
| {2, Phase::ADD}, {2, Phase::CHANGE}, {2, Phase::CANCEL}}, // 4: two pointers successive streams |
| {{2, Phase::ADD}, {1, Phase::ADD}, {2, Phase::CHANGE}, |
| {1, Phase::CHANGE}, {1, Phase::CANCEL}, {2, Phase::CANCEL}}, // 5: two pointer interleaved |
| }; |
| // clang-format on |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(InjectionWithGoodEventStream_ShouldHaveNoProblems_CombinedEvents, |
| InjectorGoodEventStreamTest, testing::ValuesIn(GoodStreamTestData())); |
| |
| INSTANTIATE_TEST_SUITE_P(InjectionWithGoodEventStream_ShouldHaveNoProblems_SeparateEvents, |
| InjectorGoodEventStreamTest, testing::ValuesIn(GoodStreamTestData())); |
| |
| // Inject a valid event stream in a single Inject() call. |
| TEST_P(InjectorGoodEventStreamTest, |
| InjectionWithGoodEventStream_ShouldHaveNoProblems_CombinedEvents) { |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool error_callback_fired = false; |
| injector.set_error_handler([&error_callback_fired](zx_status_t) { error_callback_fired = true; }); |
| |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/ |
| [](auto...) { return true; }, // Always true. |
| /*inject=*/ |
| [](auto...) {}, |
| /*on_channel_closed=*/[] {}); |
| |
| std::vector<InjectionEvent> events; |
| for (auto [pointer_id, phase] : GetParam()) { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_pointer_id(pointer_id); |
| event.mutable_data()->pointer_sample().set_phase(phase); |
| events.emplace_back(std::move(event)); |
| } |
| |
| bool injection_callback_fired = false; |
| injector->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| RunLoopUntilIdle(); |
| |
| EXPECT_TRUE(injection_callback_fired); |
| EXPECT_FALSE(error_callback_fired); |
| } |
| |
| // Inject a valid event stream in multiple Inject() calls. |
| TEST_P(InjectorGoodEventStreamTest, |
| InjectionWithGoodEventStream_ShouldHaveNoProblems_SeparateEvents) { |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool error_callback_fired = false; |
| injector.set_error_handler([&error_callback_fired](zx_status_t) { error_callback_fired = true; }); |
| |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/ |
| [](auto...) { return true; }, // Always true. |
| /*inject=*/ |
| [](auto...) {}, |
| /*on_channel_closed=*/[] {}); |
| |
| for (auto [pointer_id, phase] : GetParam()) { |
| bool injection_callback_fired = false; |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_pointer_id(pointer_id); |
| event.mutable_data()->pointer_sample().set_phase(phase); |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| RunLoopUntilIdle(); |
| |
| EXPECT_TRUE(injection_callback_fired); |
| ASSERT_FALSE(error_callback_fired); |
| } |
| } |
| |
| // Bad event streams. |
| // Each invocation gets a vector of pairs of pointer ids and Phases, representing pointer streams. |
| class InjectorBadEventStreamTest |
| : public gtest::TestLoopFixture, |
| public testing::WithParamInterface<std::vector<std::pair</*pointer_id*/ uint32_t, Phase>>> {}; |
| |
| static std::vector<std::vector<std::pair<uint32_t, Phase>>> BadStreamTestData() { |
| // clang-format off |
| return { |
| {{1, Phase::CHANGE}}, // 0: one pointer non-add initial event |
| {{1, Phase::REMOVE}}, // 1: one pointer non-add initial event |
| {{1, Phase::ADD}, {1, Phase::ADD}}, // 2: one pointer double add |
| {{1, Phase::ADD}, {1, Phase::CHANGE}, {1, Phase::ADD}}, // 3: one pointer double add mid-stream |
| {{1, Phase::ADD}, {1, Phase::REMOVE}, {1, Phase::REMOVE}}, // 4: one pointer double remove |
| {{1, Phase::ADD}, {1, Phase::REMOVE}, {1, Phase::CHANGE}}, // 5: one pointer event after remove |
| {{1, Phase::ADD}, {1, Phase::CHANGE}, |
| {1, Phase::REMOVE}, {2, Phase::ADD}, {2, Phase::ADD}}, // 6: two pointer faulty stream after correct stream |
| {{1, Phase::ADD}, {2, Phase::ADD}, |
| {2, Phase::CHANGE}, {2, Phase::REMOVE}, {1, Phase::ADD}}, // 7 two pointer faulty stream interleaved with correct stream |
| }; |
| // clang-format on |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(InjectionWithBadEventStream_ShouldCloseChannel_CombinedEvents, |
| InjectorBadEventStreamTest, testing::ValuesIn(BadStreamTestData())); |
| |
| INSTANTIATE_TEST_SUITE_P(InjectionWithBadEventStream_ShouldCloseChannel_SeparateEvents, |
| InjectorBadEventStreamTest, testing::ValuesIn(BadStreamTestData())); |
| |
| // Inject an invalid event stream in a single Inject() call. |
| TEST_P(InjectorBadEventStreamTest, InjectionWithBadEventStream_ShouldCloseChannel_CombinedEvents) { |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool error_callback_fired = false; |
| zx_status_t error = ZX_OK; |
| injector.set_error_handler([&error_callback_fired, &error](zx_status_t status) { |
| error_callback_fired = true; |
| error = status; |
| }); |
| |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/[](auto...) { return true; }, |
| /*inject=*/[](auto...) {}, |
| /*on_channel_closed=*/[] {}); |
| |
| InjectionEvent event = InjectionEventTemplate(); |
| |
| // Run event stream. |
| std::vector<InjectionEvent> events; |
| for (auto [pointer_id, phase] : GetParam()) { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_pointer_id(pointer_id); |
| event.mutable_data()->pointer_sample().set_phase(phase); |
| events.emplace_back(std::move(event)); |
| } |
| injector->Inject({std::move(events)}, [] {}); |
| RunLoopUntilIdle(); |
| |
| EXPECT_TRUE(error_callback_fired); |
| EXPECT_EQ(error, ZX_ERR_BAD_STATE); |
| } |
| |
| // Inject an invalid event stream in multiple Inject() calls. |
| TEST_P(InjectorBadEventStreamTest, InjectionWithBadEventStream_ShouldCloseChannel_SeparateEvents) { |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool error_callback_fired = false; |
| zx_status_t error = ZX_OK; |
| injector.set_error_handler([&error_callback_fired, &error](zx_status_t status) { |
| error_callback_fired = true; |
| error = status; |
| }); |
| |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/[](auto...) { return true; }, |
| /*inject=*/[](auto...) {}, |
| /*on_channel_closed=*/[] {}); |
| |
| // Run event stream. |
| for (auto [pointer_id, phase] : GetParam()) { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_pointer_id(pointer_id); |
| event.mutable_data()->pointer_sample().set_phase(phase); |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, [] {}); |
| RunLoopUntilIdle(); |
| } |
| |
| EXPECT_TRUE(error_callback_fired); |
| EXPECT_EQ(error, ZX_ERR_BAD_STATE); |
| } |
| |
| TEST(InjectorTest, InjectedViewport_ShouldNotTriggerInjectLambda) { |
| async::TestLoop test_loop; |
| |
| // Set up an isolated Injector. |
| DevicePtr injector; |
| |
| bool error_callback_fired = false; |
| injector.set_error_handler([&error_callback_fired](zx_status_t) { error_callback_fired = true; }); |
| |
| bool inject_lambda_fired = false; |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/[](zx_koid_t, zx_koid_t) { return true; }, |
| /*inject=*/[&inject_lambda_fired](auto...) { inject_lambda_fired = true; }, |
| /*on_channel_closed=*/[] {}); |
| |
| { |
| bool injection_callback_fired = false; |
| InjectionEvent event; |
| event.set_timestamp(1); |
| { |
| fuchsia::ui::pointerinjector::Viewport viewport; |
| viewport.set_extents({{{-242, -383}, {124, 252}}}); |
| viewport.set_viewport_to_context_transform(kIdentityMatrix); |
| fuchsia::ui::pointerinjector::Data data; |
| data.set_viewport(std::move(viewport)); |
| event.set_data(std::move(data)); |
| } |
| |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| test_loop.RunUntilIdle(); |
| EXPECT_TRUE(injection_callback_fired); |
| } |
| |
| test_loop.RunUntilIdle(); |
| |
| EXPECT_FALSE(inject_lambda_fired); |
| EXPECT_FALSE(error_callback_fired); |
| } |
| |
| // Parameterized tests for malformed viewport arguments. |
| // Use pairs of optional extents and matrices. Because test parameters must be copyable. |
| using ViewportPair = std::pair<std::optional<std::array<std::array<float, 2>, 2>>, |
| std::optional<std::array<float, 9>>>; |
| class InjectorBadViewportTest : public gtest::TestLoopFixture, |
| public testing::WithParamInterface<ViewportPair> {}; |
| |
| static std::vector<ViewportPair> BadViewportTestData() { |
| std::vector<ViewportPair> bad_viewports; |
| { // 0: No extents. |
| ViewportPair pair; |
| pair.second.emplace(kIdentityMatrix); |
| bad_viewports.emplace_back(pair); |
| } |
| { // 1: No viewport_to_context_transform. |
| ViewportPair pair; |
| pair.first = {{/*min*/ {0, 0}, /*max*/ {10, 10}}}; |
| bad_viewports.emplace_back(pair); |
| } |
| { // 2: Malformed extents: Min bigger than max. |
| fuchsia::ui::pointerinjector::Viewport viewport; |
| ViewportPair pair; |
| pair.first = {{/*min*/ {-100, 100}, /*max*/ {100, -100}}}; |
| pair.second = kIdentityMatrix; |
| bad_viewports.emplace_back(pair); |
| } |
| { // 3: Malformed extents: Min equal to max. |
| fuchsia::ui::pointerinjector::Viewport viewport; |
| ViewportPair pair; |
| pair.first = {{/*min*/ {0, -100}, /*max*/ {0, 100}}}; |
| pair.second = kIdentityMatrix; |
| bad_viewports.emplace_back(pair); |
| } |
| { // 4: Malformed extents: Contains NaN |
| fuchsia::ui::pointerinjector::Viewport viewport; |
| ViewportPair pair; |
| pair.first = {{/*min*/ {0, 0}, /*max*/ {100, std::numeric_limits<double>::quiet_NaN()}}}; |
| pair.second = kIdentityMatrix; |
| bad_viewports.emplace_back(pair); |
| } |
| { // 5: Malformed extents: Contains Inf |
| fuchsia::ui::pointerinjector::Viewport viewport; |
| ViewportPair pair; |
| pair.first = {{/*min*/ {0, 0}, /*max*/ {100, std::numeric_limits<double>::infinity()}}}; |
| pair.second = kIdentityMatrix; |
| bad_viewports.emplace_back(pair); |
| } |
| { // 6: Malformed transform: Non-invertible matrix |
| // clang-format off |
| const std::array<float, 9> non_invertible_matrix = { |
| 1, 0, 0, |
| 1, 0, 0, |
| 0, 0, 1, |
| }; |
| // clang-format on |
| fuchsia::ui::pointerinjector::Viewport viewport; |
| ViewportPair pair; |
| pair.first = {{{/*min*/ {0, 0}, /*max*/ {10, 10}}}}; |
| pair.second = non_invertible_matrix; |
| bad_viewports.emplace_back(pair); |
| } |
| { // 7: Malformed transform: Contains NaN |
| // clang-format off |
| const std::array<float, 9> nan_matrix = { |
| 1, std::numeric_limits<double>::quiet_NaN(), 0, |
| 0, 1, 0, |
| 0, 0, 1, |
| }; |
| // clang-format on |
| fuchsia::ui::pointerinjector::Viewport viewport; |
| ViewportPair pair; |
| pair.first = {{{/*min*/ {0, 0}, /*max*/ {10, 10}}}}; |
| pair.second = nan_matrix; |
| bad_viewports.emplace_back(pair); |
| } |
| { // 8: Malformed transform: Contains Inf |
| // clang-format off |
| const std::array<float, 9> inf_matrix = { |
| 1, std::numeric_limits<double>::infinity(), 0, |
| 0, 1, 0, |
| 0, 0, 1, |
| }; |
| // clang-format on |
| fuchsia::ui::pointerinjector::Viewport viewport; |
| ViewportPair pair; |
| pair.first = {{{/*min*/ {0, 0}, /*max*/ {10, 10}}}}; |
| pair.second = inf_matrix; |
| bad_viewports.emplace_back(pair); |
| } |
| |
| return bad_viewports; |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(InjectBadViewport_ShouldCloseChannel, InjectorBadViewportTest, |
| testing::ValuesIn(BadViewportTestData())); |
| |
| TEST_P(InjectorBadViewportTest, InjectBadViewport_ShouldCloseChannel) { |
| DevicePtr injector; |
| bool error_callback_fired = false; |
| injector.set_error_handler([&error_callback_fired](zx_status_t) { error_callback_fired = true; }); |
| |
| bool inject_lambda_fired = false; |
| scenic_impl::input::TouchInjector injector_impl( |
| inspect::Node(), InjectorSettingsTemplate(), ViewportTemplate(), injector.NewRequest(), |
| /*is_descendant_and_connected=*/[](zx_koid_t, zx_koid_t) { return true; }, |
| /*inject=*/[&inject_lambda_fired](auto...) { inject_lambda_fired = true; }, |
| /*on_channel_closed=*/[] {}); |
| |
| InjectionEvent event; |
| { |
| event.set_timestamp(1); |
| fuchsia::ui::pointerinjector::Data data; |
| ViewportPair params = GetParam(); |
| fuchsia::ui::pointerinjector::Viewport viewport; |
| if (params.first) |
| viewport.set_extents(params.first.value()); |
| if (params.second) |
| viewport.set_viewport_to_context_transform(params.second.value()); |
| data.set_viewport(std::move(viewport)); |
| event.set_data(std::move(data)); |
| } |
| |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| bool injection_callback_fired = false; |
| injector->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(injection_callback_fired); |
| EXPECT_TRUE(error_callback_fired); |
| } |
| |
| class InjectorInspectionTest : public gtest::TestLoopFixture { |
| protected: |
| void SetUp() override { |
| injector_impl_.emplace( |
| inspector_.GetRoot().CreateChild("injector"), InjectorSettingsTemplate(), |
| ViewportTemplate(), injector_.NewRequest(), |
| /*is_descendant_and_connected=*/[](auto...) { return true; }, |
| /*inject=*/[this](auto...) { ++num_injections_; }, |
| /*on_channel_closed=*/[] {}); |
| } |
| |
| std::pair<inspect::Hierarchy, const inspect::Hierarchy*> ReadHierarchyFromInspector() { |
| fpromise::result<inspect::Hierarchy> result; |
| fpromise::single_threaded_executor exec; |
| exec.schedule_task( |
| inspect::ReadFromInspector(inspector_).then([&](fpromise::result<inspect::Hierarchy>& res) { |
| result = std::move(res); |
| })); |
| exec.run(); |
| |
| inspect::Hierarchy root = result.take_value(); |
| const inspect::Hierarchy* hierarchy = root.GetByPath({"injector"}); |
| FX_DCHECK(hierarchy); |
| return {std::move(root), hierarchy}; |
| } |
| |
| std::vector<inspect::UintArrayValue::HistogramBucket> GetHistogramBuckets( |
| const std::string& property) { |
| const auto [root, parent] = ReadHierarchyFromInspector(); |
| const inspect::UintArrayValue* histogram = |
| parent->node().get_property<inspect::UintArrayValue>(property); |
| FX_CHECK(histogram) << "no histogram named " << property << " found"; |
| return histogram->GetBuckets(); |
| } |
| |
| uint64_t GetInjectionsAtMinute(uint64_t minute) { |
| const auto [root, parent] = ReadHierarchyFromInspector(); |
| const inspect::Hierarchy* node = parent->GetByPath({kHistoryNodeName}); |
| FX_CHECK(node); |
| const inspect::UintPropertyValue* count = node->node().get_property<inspect::UintPropertyValue>( |
| "Events at minute " + std::to_string(minute)); |
| if (count) { |
| return count->value(); |
| } else { |
| FX_LOGS(INFO) << "Found no data for minute " << minute; |
| ; |
| return 0; |
| } |
| } |
| |
| uint64_t GetTotalInjections() { |
| const auto [root, parent] = ReadHierarchyFromInspector(); |
| const inspect::Hierarchy* node = parent->GetByPath({kHistoryNodeName}); |
| FX_CHECK(node); |
| const inspect::UintPropertyValue* total = |
| node->node().get_property<inspect::UintPropertyValue>("Total"); |
| FX_CHECK(total); |
| return total->value(); |
| } |
| |
| const std::string kHistoryNodeName = |
| "Last " + std::to_string(scenic_impl::input::InjectorInspector::kNumMinutesOfHistory) + |
| " minutes of injected events"; |
| inspect::Inspector inspector_; |
| DevicePtr injector_; |
| uint64_t num_injections_ = 0; |
| std::optional<scenic_impl::input::TouchInjector> injector_impl_; |
| }; |
| |
| TEST_F(InjectorInspectionTest, HistogramsTrackInjections) { |
| bool error_callback_fired = false; |
| injector_.set_error_handler( |
| [&error_callback_fired](zx_status_t) { error_callback_fired = true; }); |
| |
| { // Inject ADD event. |
| bool injection_callback_fired = false; |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::ADD); |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector_->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(injection_callback_fired); |
| |
| EXPECT_EQ(num_injections_, 1u); |
| EXPECT_FALSE(error_callback_fired); |
| } |
| |
| { // Inject CHANGE event. |
| bool injection_callback_fired = false; |
| std::vector<InjectionEvent> events; |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::CHANGE); |
| events.emplace_back(std::move(event)); |
| injector_->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(injection_callback_fired); |
| |
| EXPECT_EQ(num_injections_, 2u); |
| EXPECT_FALSE(error_callback_fired); |
| } |
| |
| { // Inject REMOVE event. |
| bool injection_callback_fired = false; |
| std::vector<InjectionEvent> events; |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::REMOVE); |
| events.emplace_back(std::move(event)); |
| injector_->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(injection_callback_fired); |
| |
| EXPECT_EQ(num_injections_, 3u); |
| EXPECT_FALSE(error_callback_fired); |
| } |
| |
| { // Inject VIEWPORT event. |
| bool injection_callback_fired = false; |
| InjectionEvent event; |
| event.set_timestamp(1); |
| { |
| fuchsia::ui::pointerinjector::Viewport viewport; |
| viewport.set_extents({{{-242, -383}, {124, 252}}}); |
| viewport.set_viewport_to_context_transform(kIdentityMatrix); |
| fuchsia::ui::pointerinjector::Data data; |
| data.set_viewport(std::move(viewport)); |
| event.set_data(std::move(data)); |
| } |
| |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector_->Inject({std::move(events)}, |
| [&injection_callback_fired] { injection_callback_fired = true; }); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(injection_callback_fired); |
| |
| // Still 3 injections; the callback is not invoked for viewport changes. |
| EXPECT_EQ(num_injections_, 3u); |
| EXPECT_FALSE(error_callback_fired); |
| } |
| |
| { |
| uint64_t count = 0; |
| for (const inspect::UintArrayValue::HistogramBucket& bucket : |
| GetHistogramBuckets("viewport_event_latency_usecs")) { |
| count += bucket.count; |
| } |
| |
| EXPECT_EQ(count, 1u); |
| } |
| |
| { |
| uint64_t count = 0; |
| for (const inspect::UintArrayValue::HistogramBucket& bucket : |
| GetHistogramBuckets("pointer_event_latency_usecs")) { |
| count += bucket.count; |
| } |
| |
| EXPECT_EQ(count, 3u); |
| } |
| } |
| |
| TEST_F(InjectorInspectionTest, InspectHistory) { |
| const uint64_t kMaxNum = scenic_impl::input::InjectorInspector::kNumMinutesOfHistory; |
| ASSERT_TRUE(kMaxNum > 2) << "This test assumes a minimum length of history"; |
| |
| const uint64_t start_minute = Now().get() / zx::min(1).get(); |
| |
| bool error_callback_fired = false; |
| injector_.set_error_handler( |
| [&error_callback_fired](zx_status_t) { error_callback_fired = true; }); |
| |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute), 0u); |
| EXPECT_EQ(GetTotalInjections(), 0u); |
| |
| // Inject events. Each one should register in inspect. |
| { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::ADD); |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector_->Inject({std::move(events)}, [] {}); |
| RunLoopUntilIdle(); |
| } |
| |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute), 1u); |
| EXPECT_EQ(GetTotalInjections(), 1u); |
| |
| { |
| std::vector<InjectionEvent> events; |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::CHANGE); |
| events.emplace_back(std::move(event)); |
| injector_->Inject({std::move(events)}, [] {}); |
| RunLoopUntilIdle(); |
| } |
| |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute), 2u); |
| EXPECT_EQ(GetTotalInjections(), 2u); |
| |
| { |
| std::vector<InjectionEvent> events; |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::CHANGE); |
| events.emplace_back(std::move(event)); |
| injector_->Inject({std::move(events)}, [] {}); |
| RunLoopUntilIdle(); |
| } |
| |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute), 3u); |
| EXPECT_EQ(GetTotalInjections(), 3u); |
| |
| { // Inject VIEWPORT event. It should not be reflected in the injection stats. |
| InjectionEvent event; |
| event.set_timestamp(1); |
| { |
| fuchsia::ui::pointerinjector::Viewport viewport; |
| viewport.set_extents({{{-242, -383}, {124, 252}}}); |
| viewport.set_viewport_to_context_transform(kIdentityMatrix); |
| fuchsia::ui::pointerinjector::Data data; |
| data.set_viewport(std::move(viewport)); |
| event.set_data(std::move(data)); |
| } |
| |
| std::vector<InjectionEvent> events; |
| events.emplace_back(std::move(event)); |
| injector_->Inject({std::move(events)}, [] {}); |
| RunLoopUntilIdle(); |
| } |
| |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute), 3u); |
| EXPECT_EQ(GetTotalInjections(), 3u); |
| |
| // Roll forward one minute, inject an event and observe that history has updated correctly. |
| RunLoopFor(zx::min(1)); |
| { |
| std::vector<InjectionEvent> events; |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::CHANGE); |
| events.emplace_back(std::move(event)); |
| injector_->Inject({std::move(events)}, [] {}); |
| RunLoopUntilIdle(); |
| } |
| |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute), 3u); |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute + 1), 1u); |
| EXPECT_EQ(GetTotalInjections(), 4u); |
| |
| // Roll forward one less than the size of the ringbuffer. Now the start minute should have |
| // disappeared, but not the second minute. |
| RunLoopFor(zx::min(kMaxNum - 1)); |
| |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute), 0u); |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute + 1), 1u); |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute + kMaxNum), 0u); |
| EXPECT_EQ(GetTotalInjections(), 1u); |
| |
| { |
| std::vector<InjectionEvent> events; |
| { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::CHANGE); |
| events.emplace_back(std::move(event)); |
| } |
| { |
| InjectionEvent event = InjectionEventTemplate(); |
| event.mutable_data()->pointer_sample().set_phase(Phase::CHANGE); |
| events.emplace_back(std::move(event)); |
| } |
| injector_->Inject({std::move(events)}, [] {}); |
| RunLoopUntilIdle(); |
| } |
| |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute), 0u); |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute + 1), 1u); |
| EXPECT_EQ(GetInjectionsAtMinute(start_minute + kMaxNum), 2u); |
| EXPECT_EQ(GetTotalInjections(), 3u); |
| } |
| |
| } // namespace input::test |