Mir

Merge lp:~andreas-pokorny/mir/0.20-fix-1550050 into lp:mir/0.20

Proposed by Andreas Pokorny
Status: Merged
Merged at revision: 3331
Proposed branch: lp:~andreas-pokorny/mir/0.20-fix-1550050
Merge into: lp:mir/0.20
Diff against target: 423 lines (+219/-53)
6 files modified
include/test/mir/test/doubles/mock_input_device_hub.h (+43/-0)
src/server/input/default_configuration.cpp (+37/-24)
src/server/input/default_input_device_hub.cpp (+14/-8)
src/server/input/key_repeat_dispatcher.cpp (+41/-0)
src/server/input/key_repeat_dispatcher.h (+7/-4)
tests/unit-tests/input/test_key_repeat_dispatcher.cpp (+77/-17)
To merge this branch: bzr merge lp:~andreas-pokorny/mir/0.20-fix-1550050
Reviewer Review Type Date Requested Status
Mir CI Bot continuous-integration Needs Fixing
Kevin DuBois (community) Approve
Review via email: mp+287854@code.launchpad.net

Commit message

Fixes the potential endless repeat on unplugged devices

The key repeat dispatcher now reacts on the removal of input devices by resetting potentially still active repeat alarms. The integration here is a bit clumsy, since the input device hub needs the input dispatcher to forward device events. While the input dispatcher needs it to register at the input device hub as an observer.

Description of the change

This is the small version of the fix. It makes the KeyRepeatDispatcher observe the input devices attached and resets the repeat handling alarms on device removal.

The changes in src/server/input/default_configuration.cpp were necessary since there is already a dependency path from InputDeviceHub to InputDispatcher. I plan to make a different fix on lp:mir that moves the repeat handling directly to the device/platform itself.

To post a comment you must log in.
Revision history for this message
Andreas Pokorny (andreas-pokorny) wrote :

The effect of the branch is sometimes hidden by the fact that some bluetooth drivers keep track for input states and release keys when unplugging the device. The problem is easier to reproduce with USB devices.

Revision history for this message
Kevin DuBois (kdub) wrote :

src/server/input/default_configuration.cpp

- [this]()
+ [this]() -> std::shared_ptr<mi::InputDispatcher>

- the_event_filter_chain_dispatcher(), the_main_loop(), the_cookie_authority(),
- enable_repeat, key_repeat_timeout, key_repeat_delay);
+ the_event_filter_chain_dispatcher(), the_main_loop(), the_cookie_authority(),
+ enable_repeat, key_repeat_timeout, key_repeat_delay);

unneeded changes?

+ void set_input_device_hub(std::shared_ptr<InputDeviceHub> const& hub);
two step initialization.

review: Needs Fixing
Revision history for this message
Andreas Pokorny (andreas-pokorny) wrote :

> src/server/input/default_configuration.cpp
>
> - [this]()
> + [this]() -> std::shared_ptr<mi::InputDispatcher>
>
> - the_event_filter_chain_dispatcher(), the_main_loop(),
> the_cookie_authority(),
> - enable_repeat, key_repeat_timeout, key_repeat_delay);
> + the_event_filter_chain_dispatcher(), the_main_loop(),
> the_cookie_authority(),
> + enable_repeat, key_repeat_timeout, key_repeat_delay);
>
> unneeded changes?

yes reverting that..

>
> + void set_input_device_hub(std::shared_ptr<InputDeviceHub> const& hub);
> two step initialization.

This is one of the reasons why I make a better solution into lp:mir - it will turn into a bigger change and somewhat turn into something that no longer looks like simple fix.
Two step init is necessary to avoid a dependency cycle between 'the intput dispatchers' and InputDeviceHub.

Revision history for this message
Kevin DuBois (kdub) wrote :

I suppose I don't mind as much if the two step initialization will be improved before going to trunk.

It would be good (necessary?) to see a CI pass on the MP before merging as the release. I suppose all that adds up to a +1 from me then.

review: Approve
Revision history for this message
Andreas Pokorny (andreas-pokorny) wrote :

> I suppose I don't mind as much if the two step initialization will be improved
> before going to trunk.
>
> It would be good (necessary?) to see a CI pass on the MP before merging as the
> release. I suppose all that adds up to a +1 from me then.

Yeah our ci has yet no way of running against a release branch.. Maybe a manually triggered run could work

Revision history for this message
Mir CI Bot (mir-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Mir CI Bot (mir-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Andreas Pokorny (andreas-pokorny) wrote :

ok does not work that way..

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'include/test/mir/test/doubles/mock_input_device_hub.h'
--- include/test/mir/test/doubles/mock_input_device_hub.h 1970-01-01 00:00:00 +0000
+++ include/test/mir/test/doubles/mock_input_device_hub.h 2016-03-03 16:17:57 +0000
@@ -0,0 +1,43 @@
1/*
2 * Copyright © 2016 Canonical Ltd.
3 *
4 * This program is free software: you can redistribute it and/or modify it
5 * under the terms of the GNU General Public License version 3,
6 * as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * Authored by: Andreas Pokorny <andreas.pokorny@canonical.com>
17 */
18
19#ifndef MIR_TEST_DOUBLES_MOCK_INPUT_DEVICE_HUB_H_
20#define MIR_TEST_DOUBLES_MOCK_INPUT_DEVICE_HUB_H_
21
22#include "mir/input/input_device_hub.h"
23
24namespace mir
25{
26namespace test
27{
28namespace doubles
29{
30
31struct MockInputDeviceHub : input::InputDeviceHub
32{
33 MOCK_METHOD1(add_observer, void(std::shared_ptr<input::InputDeviceObserver> const&));
34 MOCK_METHOD1(remove_observer, void(std::weak_ptr<input::InputDeviceObserver> const&));
35 MOCK_METHOD1(for_each_input_device, void(std::function<void(std::shared_ptr<input::Device>const&)> const&));
36
37};
38
39}
40}
41}
42
43#endif
044
=== modified file 'src/server/input/default_configuration.cpp'
--- src/server/input/default_configuration.cpp 2016-02-12 10:51:29 +0000
+++ src/server/input/default_configuration.cpp 2016-03-03 16:17:57 +0000
@@ -270,33 +270,46 @@
270270
271std::shared_ptr<mi::InputDeviceRegistry> mir::DefaultServerConfiguration::the_input_device_registry()271std::shared_ptr<mi::InputDeviceRegistry> mir::DefaultServerConfiguration::the_input_device_registry()
272{272{
273 return default_input_device_hub([this]()273 return default_input_device_hub(
274 {274 [this]()
275 return std::make_shared<mi::DefaultInputDeviceHub>(275 {
276 std::make_shared<mi::BasicSeat>(276 auto input_dispatcher = the_input_dispatcher();
277 the_input_dispatcher(),277 auto key_repeater = std::dynamic_pointer_cast<mi::KeyRepeatDispatcher>(input_dispatcher);
278 the_touch_visualizer(),278 auto hub = std::make_shared<mi::DefaultInputDeviceHub>(
279 the_cursor_listener(),279 std::make_shared<mi::BasicSeat>(
280 the_input_region()),280 input_dispatcher,
281 the_input_reading_multiplexer(),281 the_touch_visualizer(),
282 the_main_loop(),282 the_cursor_listener(),
283 the_cookie_authority());283 the_input_region()),
284 });284 the_input_reading_multiplexer(),
285 the_main_loop(),
286 the_cookie_authority());
287
288 if (key_repeater)
289 key_repeater->set_input_device_hub(hub);
290 return hub;
291 });
285}292}
286293
287std::shared_ptr<mi::InputDeviceHub> mir::DefaultServerConfiguration::the_input_device_hub()294std::shared_ptr<mi::InputDeviceHub> mir::DefaultServerConfiguration::the_input_device_hub()
288{295{
289 return default_input_device_hub([this]()296 return default_input_device_hub(
290 {297 [this]()
291 return std::make_shared<mi::DefaultInputDeviceHub>(298 {
292std::make_shared<mi::BasicSeat>(299 auto input_dispatcher = the_input_dispatcher();
293 the_input_dispatcher(),300 auto key_repeater = std::dynamic_pointer_cast<mi::KeyRepeatDispatcher>(input_dispatcher);
294 the_touch_visualizer(),301 auto hub = std::make_shared<mi::DefaultInputDeviceHub>(
295 the_cursor_listener(),302 std::make_shared<mi::BasicSeat>(
296 the_input_region()),303 input_dispatcher,
304 the_touch_visualizer(),
305 the_cursor_listener(),
306 the_input_region()),
307 the_input_reading_multiplexer(),
308 the_main_loop(),
309 the_cookie_authority());
297310
298 the_input_reading_multiplexer(),311 if (key_repeater)
299 the_main_loop(),312 key_repeater->set_input_device_hub(hub);
300 the_cookie_authority());313 return hub;
301 });314 });
302}315}
303316
=== modified file 'src/server/input/default_input_device_hub.cpp'
--- src/server/input/default_input_device_hub.cpp 2016-01-29 15:55:17 +0000
+++ src/server/input/default_input_device_hub.cpp 2016-03-03 16:17:57 +0000
@@ -228,17 +228,23 @@
228228
229void mi::DefaultInputDeviceHub::remove_device_handle(MirInputDeviceId id)229void mi::DefaultInputDeviceHub::remove_device_handle(MirInputDeviceId id)
230{230{
231 auto handle_it = remove_if(begin(handles),231 auto handle_it = remove_if(
232 end(handles),232 begin(handles),
233 [&id](auto const& handle){return handle->id() == id;});233 end(handles),
234 [this,&id](auto const& handle)
235 {
236 if (handle->id() != id)
237 return false;
238 for (auto const& observer : observers)
239 {
240 observer->device_removed(handle);
241 observer->changes_complete();
242 }
243 return true;
244 });
234245
235 if (handle_it == end(handles))246 if (handle_it == end(handles))
236 return;247 return;
237 for (auto const& observer : observers)
238 {
239 observer->device_removed(*handle_it);
240 observer->changes_complete();
241 }
242248
243 handles.erase(handle_it, end(handles));249 handles.erase(handle_it, end(handles));
244}250}
245251
=== modified file 'src/server/input/key_repeat_dispatcher.cpp'
--- src/server/input/key_repeat_dispatcher.cpp 2016-01-29 08:18:22 +0000
+++ src/server/input/key_repeat_dispatcher.cpp 2016-03-03 16:17:57 +0000
@@ -18,6 +18,8 @@
1818
19#include "key_repeat_dispatcher.h"19#include "key_repeat_dispatcher.h"
2020
21#include "mir/input/device.h"
22#include "mir/input/input_device_hub.h"
21#include "mir/time/alarm_factory.h"23#include "mir/time/alarm_factory.h"
22#include "mir/time/alarm.h"24#include "mir/time/alarm.h"
23#include "mir/events/event_private.h"25#include "mir/events/event_private.h"
@@ -31,6 +33,34 @@
3133
32namespace mi = mir::input;34namespace mi = mir::input;
3335
36namespace
37{
38struct DeviceRemovalFilter : mi::InputDeviceObserver
39{
40 DeviceRemovalFilter(std::function<void(MirInputDeviceId)> const& on_removal)
41 : on_removal(on_removal) {}
42
43 void device_added(std::shared_ptr<mi::Device> const&) override
44 {
45 }
46
47 void device_changed(std::shared_ptr<mi::Device> const&) override
48 {
49 }
50
51 void device_removed(std::shared_ptr<mi::Device> const& device) override
52 {
53 on_removal(device->id());
54 }
55
56 void changes_complete() override
57 {
58 }
59 std::function<void(MirInputDeviceId)> on_removal;
60};
61
62}
63
34mi::KeyRepeatDispatcher::KeyRepeatDispatcher(64mi::KeyRepeatDispatcher::KeyRepeatDispatcher(
35 std::shared_ptr<mi::InputDispatcher> const& next_dispatcher,65 std::shared_ptr<mi::InputDispatcher> const& next_dispatcher,
36 std::shared_ptr<mir::time::AlarmFactory> const& factory,66 std::shared_ptr<mir::time::AlarmFactory> const& factory,
@@ -47,6 +77,17 @@
47{77{
48}78}
4979
80void mi::KeyRepeatDispatcher::set_input_device_hub(std::shared_ptr<InputDeviceHub> const& hub)
81{
82 hub->add_observer(std::make_shared<DeviceRemovalFilter>(
83 [this](MirInputDeviceId id)
84 {
85 std::unique_lock<std::mutex> lock(repeat_state_mutex);
86 repeat_state_by_device.erase(id); // destructor cancels alarms
87 }
88 ));
89}
90
50mi::KeyRepeatDispatcher::KeyboardState& mi::KeyRepeatDispatcher::ensure_state_for_device_locked(std::lock_guard<std::mutex> const&, MirInputDeviceId id)91mi::KeyRepeatDispatcher::KeyboardState& mi::KeyRepeatDispatcher::ensure_state_for_device_locked(std::lock_guard<std::mutex> const&, MirInputDeviceId id)
51{92{
52 repeat_state_by_device.insert(std::make_pair(id, KeyboardState()));93 repeat_state_by_device.insert(std::make_pair(id, KeyboardState()));
5394
=== modified file 'src/server/input/key_repeat_dispatcher.h'
--- src/server/input/key_repeat_dispatcher.h 2016-01-29 08:18:22 +0000
+++ src/server/input/key_repeat_dispatcher.h 2016-03-03 16:17:57 +0000
@@ -1,5 +1,5 @@
1/*1/*
2 * Copyright © 2015 Canonical Ltd.2 * Copyright © 2015-2016 Canonical Ltd.
3 *3 *
4 * This program is free software: you can redistribute it and/or modify it4 * This program is free software: you can redistribute it and/or modify it
5 * under the terms of the GNU General Public License version 3,5 * under the terms of the GNU General Public License version 3,
@@ -20,6 +20,7 @@
20#define MIR_INPUT_KEY_REPEAT_DISPATCHER_H_20#define MIR_INPUT_KEY_REPEAT_DISPATCHER_H_
2121
22#include "mir/input/input_dispatcher.h"22#include "mir/input/input_dispatcher.h"
23#include "mir/input/input_device_observer.h"
2324
24#include <memory>25#include <memory>
25#include <chrono>26#include <chrono>
@@ -39,8 +40,8 @@
39}40}
40namespace input41namespace input
41{42{
4243class InputDeviceHub;
43class KeyRepeatDispatcher : public mir::input::InputDispatcher44class KeyRepeatDispatcher : public InputDispatcher
44{45{
45public:46public:
46 KeyRepeatDispatcher(std::shared_ptr<InputDispatcher> const& next_dispatcher,47 KeyRepeatDispatcher(std::shared_ptr<InputDispatcher> const& next_dispatcher,
@@ -54,7 +55,9 @@
54 bool dispatch(MirEvent const& event) override;55 bool dispatch(MirEvent const& event) override;
55 void start() override;56 void start() override;
56 void stop() override;57 void stop() override;
57 58
59 void set_input_device_hub(std::shared_ptr<InputDeviceHub> const& hub);
60
58private:61private:
59 std::mutex repeat_state_mutex;62 std::mutex repeat_state_mutex;
6063
6164
=== modified file 'tests/unit-tests/input/test_key_repeat_dispatcher.cpp'
--- tests/unit-tests/input/test_key_repeat_dispatcher.cpp 2016-01-29 08:18:22 +0000
+++ tests/unit-tests/input/test_key_repeat_dispatcher.cpp 2016-03-03 16:17:57 +0000
@@ -23,9 +23,15 @@
23#include "mir/time/alarm.h"23#include "mir/time/alarm.h"
24#include "mir/time/alarm_factory.h"24#include "mir/time/alarm_factory.h"
25#include "mir/cookie/authority.h"25#include "mir/cookie/authority.h"
26#include "mir/input/input_device_observer.h"
27#include "mir/input/pointer_configuration.h"
28#include "mir/input/touchpad_configuration.h"
29#include "mir/input/device.h"
2630
31#include "mir/test/fake_shared.h"
27#include "mir/test/event_matchers.h"32#include "mir/test/event_matchers.h"
28#include "mir/test/doubles/mock_input_dispatcher.h"33#include "mir/test/doubles/mock_input_dispatcher.h"
34#include "mir/test/doubles/mock_input_device_hub.h"
2935
30#include <gtest/gtest.h>36#include <gtest/gtest.h>
31#include <gmock/gmock.h>37#include <gmock/gmock.h>
@@ -35,6 +41,8 @@
35namespace mt = mir::test;41namespace mt = mir::test;
36namespace mtd = mt::doubles;42namespace mtd = mt::doubles;
3743
44using namespace ::testing;
45
38namespace46namespace
39{47{
40struct MockAlarm : public mir::time::Alarm48struct MockAlarm : public mir::time::Alarm
@@ -43,6 +51,12 @@
43 MOCK_CONST_METHOD0(state, mir::time::Alarm::State());51 MOCK_CONST_METHOD0(state, mir::time::Alarm::State());
44 MOCK_METHOD1(reschedule_in, bool(std::chrono::milliseconds));52 MOCK_METHOD1(reschedule_in, bool(std::chrono::milliseconds));
45 MOCK_METHOD1(reschedule_for, bool(mir::time::Timestamp));53 MOCK_METHOD1(reschedule_for, bool(mir::time::Timestamp));
54
55 // destructor cancels the alarm
56 ~MockAlarm()
57 {
58 cancel();
59 }
46};60};
4761
48struct MockAlarmFactory : public mir::time::AlarmFactory62struct MockAlarmFactory : public mir::time::AlarmFactory
@@ -59,25 +73,60 @@
59 }73 }
60};74};
6175
76struct StubDevice : public mi::Device
77{
78 MirInputDeviceId device_id;
79 StubDevice(MirInputDeviceId id) : device_id(id) {}
80 MirInputDeviceId id() const { return device_id;}
81 mi::DeviceCapabilities capabilities() const {return mi::DeviceCapability::keyboard;}
82 std::string name() const {return {};}
83 std::string unique_id() const {return {};}
84
85 mir::optional_value<mi::PointerConfiguration> pointer_configuration() const {return {};}
86 void apply_pointer_configuration(mi::PointerConfiguration const&) {;}
87 mir::optional_value<mi::TouchpadConfiguration> touchpad_configuration() const {return {};}
88 void apply_touchpad_configuration(mi::TouchpadConfiguration const&) {}
89};
90
62struct KeyRepeatDispatcher : public testing::Test91struct KeyRepeatDispatcher : public testing::Test
63{92{
64 KeyRepeatDispatcher()93 KeyRepeatDispatcher()
65 : dispatcher(mock_next_dispatcher, mock_alarm_factory, cookie_authority, true, repeat_time, repeat_delay)94 : dispatcher(mock_next_dispatcher, mock_alarm_factory, cookie_authority, true, repeat_time, repeat_delay)
66 {95 {
67 }96 ON_CALL(hub,add_observer(_)).WillByDefault(SaveArg<0>(&observer));
97 dispatcher.set_input_device_hub(mt::fake_shared(hub));
98 }
99 void simulate_device_removal()
100 {
101 StubDevice dev(test_device);
102 observer->device_removed(mt::fake_shared(dev));
103 observer->changes_complete();
104 }
105
106 const MirInputDeviceId test_device = 123;
68 std::shared_ptr<mtd::MockInputDispatcher> mock_next_dispatcher = std::make_shared<mtd::MockInputDispatcher>();107 std::shared_ptr<mtd::MockInputDispatcher> mock_next_dispatcher = std::make_shared<mtd::MockInputDispatcher>();
69 std::shared_ptr<MockAlarmFactory> mock_alarm_factory = std::make_shared<MockAlarmFactory>();108 std::shared_ptr<MockAlarmFactory> mock_alarm_factory = std::make_shared<MockAlarmFactory>();
70 std::shared_ptr<mir::cookie::Authority> cookie_authority = mir::cookie::Authority::create();109 std::shared_ptr<mir::cookie::Authority> cookie_authority = mir::cookie::Authority::create();
71 std::chrono::milliseconds const repeat_time{2};110 std::chrono::milliseconds const repeat_time{2};
72 std::chrono::milliseconds const repeat_delay{1};111 std::chrono::milliseconds const repeat_delay{1};
112 std::shared_ptr<mi::InputDeviceObserver> observer;
113 NiceMock<mtd::MockInputDeviceHub> hub;
73 mi::KeyRepeatDispatcher dispatcher;114 mi::KeyRepeatDispatcher dispatcher;
115
116 mir::EventUPtr a_key_down_event()
117 {
118 return mev::make_event(test_device, std::chrono::nanoseconds(0), std::vector<uint8_t>{}, mir_keyboard_action_down, 0, 0, mir_input_event_modifier_alt);
119 }
120
121 mir::EventUPtr a_key_up_event()
122 {
123 return mev::make_event(test_device, std::chrono::nanoseconds(0), std::vector<uint8_t>{}, mir_keyboard_action_up, 0, 0, mir_input_event_modifier_alt);
124 }
74};125};
75}126}
76127
77TEST_F(KeyRepeatDispatcher, forwards_start_stop)128TEST_F(KeyRepeatDispatcher, forwards_start_stop)
78{129{
79 using namespace ::testing;
80
81 InSequence seq;130 InSequence seq;
82 EXPECT_CALL(*mock_next_dispatcher, start()).Times(1);131 EXPECT_CALL(*mock_next_dispatcher, start()).Times(1);
83 EXPECT_CALL(*mock_next_dispatcher, stop()).Times(1);132 EXPECT_CALL(*mock_next_dispatcher, stop()).Times(1);
@@ -86,22 +135,8 @@
86 dispatcher.stop();135 dispatcher.stop();
87}136}
88137
89namespace
90{
91mir::EventUPtr a_key_down_event()
92{
93 return mev::make_event(0, std::chrono::nanoseconds(0), std::vector<uint8_t>{}, mir_keyboard_action_down, 0, 0, mir_input_event_modifier_alt);
94}
95mir::EventUPtr a_key_up_event()
96{
97 return mev::make_event(0, std::chrono::nanoseconds(0), std::vector<uint8_t>{}, mir_keyboard_action_up, 0, 0, mir_input_event_modifier_alt);
98}
99}
100
101TEST_F(KeyRepeatDispatcher, schedules_alarm_to_repeat_key_down)138TEST_F(KeyRepeatDispatcher, schedules_alarm_to_repeat_key_down)
102{139{
103 using namespace ::testing;
104
105 MockAlarm *mock_alarm = new MockAlarm; // deleted by AlarmFactory140 MockAlarm *mock_alarm = new MockAlarm; // deleted by AlarmFactory
106 std::function<void()> alarm_function;141 std::function<void()> alarm_function;
107142
@@ -122,3 +157,28 @@
122 // Trigger the cancel157 // Trigger the cancel
123 dispatcher.dispatch(*a_key_up_event());158 dispatcher.dispatch(*a_key_up_event());
124}159}
160
161TEST_F(KeyRepeatDispatcher, stops_repeat_on_device_removal)
162{
163 MockAlarm *mock_alarm = new MockAlarm;
164 std::function<void()> alarm_function;
165 bool alarm_canceled = false;
166
167 InSequence seq;
168 EXPECT_CALL(*mock_alarm_factory, create_alarm_adapter(_)).Times(1).
169 WillOnce(DoAll(SaveArg<0>(&alarm_function), Return(mock_alarm)));
170 // Once for initial down and again when invoked
171 EXPECT_CALL(*mock_alarm, reschedule_in(repeat_time)).Times(1).WillOnce(Return(true));
172 EXPECT_CALL(*mock_next_dispatcher, dispatch(mt::KeyDownEvent())).Times(1);
173 EXPECT_CALL(*mock_next_dispatcher, dispatch(mt::KeyRepeatEvent())).Times(1);
174 EXPECT_CALL(*mock_alarm, reschedule_in(repeat_delay)).Times(1).WillOnce(Return(true));
175 ON_CALL(*mock_alarm, cancel()).WillByDefault(Invoke([&](){alarm_canceled = true; return true;}));
176
177 dispatcher.dispatch(*a_key_down_event());
178
179 alarm_function();
180 Mock::VerifyAndClearExpectations(mock_alarm); // mock_alarm will be deleted after this
181
182 simulate_device_removal();
183 EXPECT_THAT(alarm_canceled, Eq(true));
184}

Subscribers

People subscribed via source and target branches