blob: 4ad47ed2bae3823b2a290cd4db5944763b008623 [file]
// 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 "src/developer/memory/pressure_signaler/pressure_notifier.h"
#include <fidl/fuchsia.feedback/cpp/fidl.h>
#include <fidl/fuchsia.feedback/cpp/test_base.h>
#include <fidl/fuchsia.memory.debug/cpp/fidl.h>
#include <lib/async/default.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/time.h>
#include <cstddef>
#include <gtest/gtest.h>
#include "src/lib/testing/loop_fixture/test_loop_fixture.h"
namespace pressure_signaler::test {
namespace fmp = fuchsia_memorypressure;
class CrashReporterForTest : public fidl::testing::TestBase<fuchsia_feedback::CrashReporter> {
public:
void FileReport(FileReportRequest& request, FileReportCompleter::Sync& completer) override {
num_crash_reports_++;
completer.Reply(fit::ok(fuchsia_feedback::CrashReporterFileReportResponse{}));
}
void NotImplemented_(const std::string& name, fidl::CompleterBase& completer) override {
FX_NOTIMPLEMENTED() << name << " is not implemented";
}
size_t num_crash_reports() const { return num_crash_reports_; }
private:
size_t num_crash_reports_ = 0;
};
class PressureNotifierUnitTest : public fidl::Server<fuchsia_memory_debug::MemoryPressure>,
public gtest::TestLoopFixture {
public:
void SetUp() override {
SetUpNewPressureNotifier(true /*notify_crash_reporter*/);
last_level_ = Level::kNormal;
}
protected:
void SetUpNewPressureNotifier(bool send_critical_pressure_crash_reports) {
auto endpoints = fidl::CreateEndpoints<fuchsia_feedback::CrashReporter>().value();
auto _ = fidl::BindServer<fuchsia_feedback::CrashReporter>(
dispatcher(), std::move(std::move(endpoints.server)), &crash_reporter_);
auto client =
fidl::Client<fuchsia_feedback::CrashReporter>(std::move(endpoints.client), dispatcher());
notifier_ =
std::make_unique<PressureNotifier>(false, send_critical_pressure_crash_reports,
std::move(client), async_get_default_dispatcher());
// Set up initial pressure level.
notifier_->observer_.WaitOnLevelChange();
}
fidl::Client<fmp::Provider> Provider() {
auto endpoints = fidl::CreateEndpoints<fmp::Provider>().value();
auto _ =
fidl::BindServer<fmp::Provider>(dispatcher(), std::move(endpoints.server), notifier_.get());
return fidl::Client<fmp::Provider>(std::move(endpoints.client), dispatcher());
}
size_t GetWatcherCount() { return notifier_->watchers_.size(); }
void ReleaseWatchers() {
for (auto& w : notifier_->watchers_) {
notifier_->ReleaseWatcher(w.get());
}
}
void TriggerLevelChange(Level level) {
if (level >= Level::kNumLevels) {
return;
}
FX_LOGS(INFO) << "PressureNotifierUnittest: Triggering level change to " << level;
notifier_->observer_.OnLevelChanged(notifier_->observer_.wait_items_[level].handle);
RunLoopUntilIdle();
}
void SetupMemDebugService() {
auto endpoints = fidl::CreateEndpoints<fuchsia_memory_debug::MemoryPressure>().value();
auto _ = fidl::BindServer<fuchsia_memory_debug::MemoryPressure>(
dispatcher(), std::move(endpoints.server), this);
memdebug_ = fidl::Client<fuchsia_memory_debug::MemoryPressure>(std::move(endpoints.client),
dispatcher());
}
void TestSimulatedPressure(fmp::Level level) {
auto result = memdebug_->Signal({{.level = level}});
ASSERT_TRUE(result.is_ok());
}
void SetCrashReportInterval(uint32_t mins) {
notifier_->critical_crash_report_interval_ = zx::min(mins);
}
bool CanGenerateNewCriticalCrashReports() const {
return notifier_->CanGenerateNewCriticalCrashReports();
}
size_t num_crash_reports() const { return crash_reporter_.num_crash_reports(); }
Level last_level() const { return last_level_; }
private:
void Signal(SignalRequest& request, SignalCompleter::Sync& completer) override {
notifier_->DebugNotify(request.level());
}
std::unique_ptr<PressureNotifier> notifier_;
CrashReporterForTest crash_reporter_;
Level last_level_;
fidl::Client<fuchsia_memory_debug::MemoryPressure> memdebug_;
};
class PressureWatcherForTest : public fidl::Server<fmp::Watcher> {
public:
PressureWatcherForTest(bool send_responses, async_dispatcher_t* dispatcher)
: send_responses_(send_responses) {
auto endpoints = fidl::CreateEndpoints<fmp::Watcher>();
client_ = std::move(endpoints->client);
binding_ = fidl::BindServer<fmp::Watcher>(dispatcher, std::move(endpoints->server), this);
}
~PressureWatcherForTest() override {
if (binding_)
binding_->Close({});
}
void OnLevelChanged(OnLevelChangedRequest& request,
OnLevelChangedCompleter::Sync& completer) override {
changes_++;
last_level_ = request.level();
if (send_responses_) {
completer.Reply();
} else {
stashed_cb_ = completer.ToAsync();
}
}
void Respond() { stashed_cb_->Reply(); }
void Register(fidl::Client<fmp::Provider> provider) {
auto result = provider->RegisterWatcher({{.watcher = std::move(client_)}});
ASSERT_TRUE(result.is_ok());
}
int NumChanges() const { return changes_; }
fmp::Level LastLevel() const { return last_level_; }
private:
fmp::Level last_level_;
int changes_ = 0;
bool send_responses_;
std::optional<OnLevelChangedCompleter::Async> stashed_cb_;
fidl::ClientEnd<fmp::Watcher> client_;
std::optional<fidl::ServerBindingRef<fmp::Watcher>> binding_;
};
TEST_F(PressureNotifierUnitTest, Watcher) {
// Scoped so that the Watcher gets deleted. We can then verify that the Provider has no watchers
// remaining.
{
PressureWatcherForTest watcher(true, dispatcher());
// Registering the watcher should call OnLevelChanged().
watcher.Register(Provider());
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 1ul);
ASSERT_EQ(watcher.NumChanges(), 1);
// Trigger a pressure level change, causing another call to OnLevelChanged().
TriggerLevelChange(Level::kNormal);
RunLoopUntilIdle();
ASSERT_EQ(watcher.NumChanges(), 2);
}
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 0ul);
}
TEST_F(PressureNotifierUnitTest, NoResponse) {
PressureWatcherForTest watcher(false, dispatcher());
watcher.Register(Provider());
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 1ul);
ASSERT_EQ(watcher.NumChanges(), 1);
// This should not trigger a new notification as the watcher has not responded to the last one.
TriggerLevelChange(Level::kNormal);
RunLoopUntilIdle();
ASSERT_EQ(watcher.NumChanges(), 1);
}
TEST_F(PressureNotifierUnitTest, DelayedResponse) {
PressureWatcherForTest watcher(false, dispatcher());
// Signal a specific pressure level here, so that the next one can be different. Delayed callbacks
// will only come through if the client has missed a level that wasn't the same as the previous
// one it received a signal for.
TriggerLevelChange(Level::kNormal);
watcher.Register(Provider());
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 1ul);
ASSERT_EQ(watcher.NumChanges(), 1);
// This should not trigger a new notification as the watcher has not responded to the last one.
TriggerLevelChange(Level::kWarning);
RunLoopUntilIdle();
ASSERT_EQ(watcher.NumChanges(), 1);
// Respond to the last message. This should send a new notification to the watcher.
watcher.Respond();
RunLoopUntilIdle();
ASSERT_EQ(watcher.NumChanges(), 2);
}
TEST_F(PressureNotifierUnitTest, MultipleWatchers) {
// Scoped so that the Watcher gets deleted. We can then verify that the Provider has no watchers
// remaining.
{
PressureWatcherForTest watcher1(true, dispatcher());
PressureWatcherForTest watcher2(true, dispatcher());
// Registering the watchers should call OnLevelChanged().
watcher1.Register(Provider());
watcher2.Register(Provider());
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 2ul);
ASSERT_EQ(watcher1.NumChanges(), 1);
ASSERT_EQ(watcher2.NumChanges(), 1);
// Trigger pressure level change, causing another call to OnLevelChanged().
TriggerLevelChange(Level::kNormal);
RunLoopUntilIdle();
ASSERT_EQ(watcher1.NumChanges(), 2);
ASSERT_EQ(watcher2.NumChanges(), 2);
}
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 0ul);
}
TEST_F(PressureNotifierUnitTest, MultipleWatchersNoResponse) {
PressureWatcherForTest watcher1(false, dispatcher());
PressureWatcherForTest watcher2(false, dispatcher());
watcher1.Register(Provider());
watcher2.Register(Provider());
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 2ul);
ASSERT_EQ(watcher1.NumChanges(), 1);
ASSERT_EQ(watcher2.NumChanges(), 1);
// This should not trigger new notifications as the watchers have not responded to the last one.
TriggerLevelChange(Level::kNormal);
RunLoopUntilIdle();
ASSERT_EQ(watcher1.NumChanges(), 1);
ASSERT_EQ(watcher2.NumChanges(), 1);
}
TEST_F(PressureNotifierUnitTest, MultipleWatchersDelayedResponse) {
PressureWatcherForTest watcher1(false, dispatcher());
PressureWatcherForTest watcher2(false, dispatcher());
// Signal a specific pressure level here, so that the next one can be different. Delayed callbacks
// will only come through if the client has missed a level that wasn't the same as the previous
// one it received a signal for.
TriggerLevelChange(Level::kNormal);
watcher1.Register(Provider());
watcher2.Register(Provider());
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 2ul);
ASSERT_EQ(watcher1.NumChanges(), 1);
ASSERT_EQ(watcher2.NumChanges(), 1);
// This should not trigger new notifications as the watchers have not responded to the last one.
TriggerLevelChange(Level::kWarning);
RunLoopUntilIdle();
ASSERT_EQ(watcher1.NumChanges(), 1);
ASSERT_EQ(watcher2.NumChanges(), 1);
// Respond to the last message. This should send new notifications to the watchers.
watcher1.Respond();
watcher2.Respond();
RunLoopUntilIdle();
ASSERT_EQ(watcher1.NumChanges(), 2);
ASSERT_EQ(watcher2.NumChanges(), 2);
}
TEST_F(PressureNotifierUnitTest, MultipleWatchersMixedResponse) {
// Set up watcher1 to not respond immediately, and watcher2 to respond immediately.
PressureWatcherForTest watcher1(false, dispatcher());
PressureWatcherForTest watcher2(true, dispatcher());
// Signal a specific pressure level here, so that the next one can be different. Delayed callbacks
// will only come through if the client has missed a level that wasn't the same as the previous
// one it received a signal for.
TriggerLevelChange(Level::kNormal);
watcher1.Register(Provider());
watcher2.Register(Provider());
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 2ul);
ASSERT_EQ(watcher1.NumChanges(), 1);
ASSERT_EQ(watcher2.NumChanges(), 1);
// Trigger pressure level change.
TriggerLevelChange(Level::kWarning);
RunLoopUntilIdle();
// Since watcher1 did not respond to the previous change, it will not see this change.
ASSERT_EQ(watcher1.NumChanges(), 1);
// Since watcher2 responded to the previous change, it will see it.
ASSERT_EQ(watcher2.NumChanges(), 2);
// watcher1 responds now.
watcher1.Respond();
RunLoopUntilIdle();
// watcher1 sees the previous change now.
ASSERT_EQ(watcher1.NumChanges(), 2);
ASSERT_EQ(watcher2.NumChanges(), 2);
}
TEST_F(PressureNotifierUnitTest, ReleaseWatcherNoPendingCallback) {
PressureWatcherForTest watcher(true, dispatcher());
watcher.Register(Provider());
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 1ul);
ASSERT_EQ(watcher.NumChanges(), 1);
// Trigger pressure level change, causing another call to OnLevelChanged().
TriggerLevelChange(Level::kNormal);
RunLoopUntilIdle();
ASSERT_EQ(watcher.NumChanges(), 2);
// Release all registered watchers, so that the watcher is now invalid.
ReleaseWatchers();
RunLoopUntilIdle();
// There were no outstanding callbacks, so ReleaseWatchers() sould have freed all watchers.
ASSERT_EQ(GetWatcherCount(), 0ul);
}
TEST_F(PressureNotifierUnitTest, ReleaseWatcherPendingCallback) {
PressureWatcherForTest watcher(false, dispatcher());
watcher.Register(Provider());
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 1ul);
ASSERT_EQ(watcher.NumChanges(), 1);
// This should not trigger a new notification as the watcher has not responded to the last one.
TriggerLevelChange(Level::kNormal);
RunLoopUntilIdle();
ASSERT_EQ(watcher.NumChanges(), 1);
// Release all registered watchers, so that the watcher is now invalid.
ReleaseWatchers();
RunLoopUntilIdle();
// Verify that the watcher has not been freed yet, since a callback is outstanding.
ASSERT_EQ(GetWatcherCount(), 1ul);
// Respond now. This should free the watcher as well.
watcher.Respond();
RunLoopUntilIdle();
// Verify that the watcher has been freed.
ASSERT_EQ(GetWatcherCount(), 0ul);
}
TEST_F(PressureNotifierUnitTest, WatcherDoesNotSeeImminentOOM) {
PressureWatcherForTest watcher(true, dispatcher());
TriggerLevelChange(Level::kImminentOOM);
watcher.Register(Provider());
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 1ul);
ASSERT_EQ(watcher.NumChanges(), 1);
// Watcher sees the initial level as Critical even though it was Imminent-OOM.
ASSERT_EQ(watcher.LastLevel(), fmp::Level::kCritical);
TriggerLevelChange(Level::kWarning);
RunLoopUntilIdle();
ASSERT_EQ(watcher.NumChanges(), 2);
// Non Imminent-OOM levels come through as expected.
ASSERT_EQ(watcher.LastLevel(), fmp::Level::kWarning);
TriggerLevelChange(Level::kImminentOOM);
RunLoopUntilIdle();
// Watcher does not see this change as the PressureNotifier won't signal it.
ASSERT_EQ(watcher.NumChanges(), 2);
ASSERT_EQ(watcher.LastLevel(), fmp::Level::kWarning);
}
TEST_F(PressureNotifierUnitTest, DelayedWatcherDoesNotSeeImminentOOM) {
// Don't send responses right away, but wait for the delayed callback to come through.
PressureWatcherForTest watcher(false, dispatcher());
TriggerLevelChange(Level::kNormal);
watcher.Register(Provider());
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 1ul);
ASSERT_EQ(watcher.NumChanges(), 1);
ASSERT_EQ(watcher.LastLevel(), fmp::Level::kNormal);
// This should not trigger a new notification as the watcher has not responded to the last one.
TriggerLevelChange(Level::kImminentOOM);
RunLoopUntilIdle();
ASSERT_EQ(watcher.NumChanges(), 1);
ASSERT_EQ(watcher.LastLevel(), fmp::Level::kNormal);
// Respond to the last message. This should send a new notification to the watcher.
watcher.Respond();
RunLoopUntilIdle();
ASSERT_EQ(watcher.NumChanges(), 2);
// Watcher will see the delayed Imminent-OOM level as Critical.
ASSERT_EQ(watcher.LastLevel(), fmp::Level::kCritical);
}
TEST_F(PressureNotifierUnitTest, CrashReportOnCritical) {
ASSERT_EQ(num_crash_reports(), 0ul);
ASSERT_TRUE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kCritical);
ASSERT_EQ(num_crash_reports(), 1ul);
ASSERT_FALSE(CanGenerateNewCriticalCrashReports());
}
TEST_F(PressureNotifierUnitTest, NoCrashReportOnWarning) {
ASSERT_EQ(num_crash_reports(), 0ul);
ASSERT_TRUE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kWarning);
ASSERT_EQ(num_crash_reports(), 0ul);
ASSERT_TRUE(CanGenerateNewCriticalCrashReports());
}
TEST_F(PressureNotifierUnitTest, NoCrashReportOnNormal) {
ASSERT_EQ(num_crash_reports(), 0ul);
ASSERT_TRUE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kNormal);
ASSERT_EQ(num_crash_reports(), 0ul);
ASSERT_TRUE(CanGenerateNewCriticalCrashReports());
}
TEST_F(PressureNotifierUnitTest, NoCrashReportOnCriticalToWarning) {
ASSERT_EQ(num_crash_reports(), 0ul);
ASSERT_TRUE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kCritical);
ASSERT_EQ(num_crash_reports(), 1ul);
ASSERT_FALSE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kWarning);
// No new crash reports for Critical -> Warning
ASSERT_EQ(num_crash_reports(), 1ul);
ASSERT_FALSE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kCritical);
// No new crash reports for Warning -> Critical
ASSERT_EQ(num_crash_reports(), 1ul);
ASSERT_FALSE(CanGenerateNewCriticalCrashReports());
}
TEST_F(PressureNotifierUnitTest, CrashReportOnCriticalToNormal) {
ASSERT_EQ(num_crash_reports(), 0ul);
ASSERT_TRUE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kCritical);
ASSERT_EQ(num_crash_reports(), 1ul);
ASSERT_FALSE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kNormal);
// No new crash reports for Critical -> Normal, but can generate future reports.
ASSERT_EQ(num_crash_reports(), 1ul);
ASSERT_TRUE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kCritical);
// New crash report generated on Critical, but cannot generate any more reports.
ASSERT_EQ(num_crash_reports(), 2ul);
ASSERT_FALSE(CanGenerateNewCriticalCrashReports());
}
TEST_F(PressureNotifierUnitTest, CrashReportOnCriticalAfterLong) {
ASSERT_EQ(num_crash_reports(), 0ul);
ASSERT_TRUE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kCritical);
ASSERT_EQ(num_crash_reports(), 1ul);
ASSERT_FALSE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kWarning);
// No new crash reports for Critical -> Warning
ASSERT_EQ(num_crash_reports(), 1ul);
ASSERT_FALSE(CanGenerateNewCriticalCrashReports());
// Crash report interval set to zero. Can generate new reports.
SetCrashReportInterval(0);
ASSERT_TRUE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kCritical);
// New crash report generated on Critical, and can generate future reports.
ASSERT_EQ(num_crash_reports(), 2ul);
ASSERT_TRUE(CanGenerateNewCriticalCrashReports());
// Crash report interval set to 30 mins. Cannot generate new reports.
SetCrashReportInterval(30);
ASSERT_FALSE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kWarning);
// No new crash reports for Critical -> Warning
ASSERT_EQ(num_crash_reports(), 2ul);
ASSERT_FALSE(CanGenerateNewCriticalCrashReports());
TriggerLevelChange(Level::kCritical);
// No new crash reports for Warning -> Critical
ASSERT_EQ(num_crash_reports(), 2ul);
ASSERT_FALSE(CanGenerateNewCriticalCrashReports());
}
TEST_F(PressureNotifierUnitTest, DoNotSendCriticalPressureCrashReport) {
SetUpNewPressureNotifier(false /*send_critical_pressure_crash_reports*/);
ASSERT_EQ(num_crash_reports(), 0ul);
ASSERT_TRUE(CanGenerateNewCriticalCrashReports());
// Cannot write critical crash reports.
TriggerLevelChange(Level::kCritical);
ASSERT_EQ(num_crash_reports(), 0ul);
// Cannot write imminent-OOM crash reports.
TriggerLevelChange(Level::kImminentOOM);
ASSERT_EQ(num_crash_reports(), 0ul);
}
TEST_F(PressureNotifierUnitTest, CrashReportOnOOM) {
ASSERT_EQ(num_crash_reports(), 0ul);
TriggerLevelChange(Level::kImminentOOM);
ASSERT_EQ(num_crash_reports(), 1ul);
}
TEST_F(PressureNotifierUnitTest, RepeatedCrashReportOnOOM) {
ASSERT_EQ(num_crash_reports(), 0ul);
TriggerLevelChange(Level::kImminentOOM);
ASSERT_EQ(num_crash_reports(), 1ul);
// Can generate repeated imminent-OOM crash reports (unlike critical ones).
TriggerLevelChange(Level::kImminentOOM);
ASSERT_EQ(num_crash_reports(), 2ul);
TriggerLevelChange(Level::kImminentOOM);
ASSERT_EQ(num_crash_reports(), 3ul);
}
TEST_F(PressureNotifierUnitTest, CrashReportOnCriticalAndOOM) {
ASSERT_EQ(num_crash_reports(), 0ul);
// Critical crash reports don't affect imminent-OOM reports.
TriggerLevelChange(Level::kCritical);
ASSERT_EQ(num_crash_reports(), 1ul);
TriggerLevelChange(Level::kImminentOOM);
ASSERT_EQ(num_crash_reports(), 2ul);
}
TEST_F(PressureNotifierUnitTest, CrashReportOnOOMAndCritical) {
ASSERT_EQ(num_crash_reports(), 0ul);
// Imminent-OOM crash reports don't affect critical reports.
TriggerLevelChange(Level::kImminentOOM);
ASSERT_EQ(num_crash_reports(), 1ul);
TriggerLevelChange(Level::kCritical);
ASSERT_EQ(num_crash_reports(), 2ul);
}
TEST_F(PressureNotifierUnitTest, SimulatePressure) {
// Scoped so that the Watcher gets deleted. We can then verify that the Provider has no watchers
// remaining.
{
PressureWatcherForTest watcher1(true, dispatcher());
PressureWatcherForTest watcher2(true, dispatcher());
// Registering the watchers should call OnLevelChanged().
watcher1.Register(Provider());
watcher2.Register(Provider());
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 2ul);
ASSERT_EQ(watcher1.NumChanges(), 1);
ASSERT_EQ(watcher2.NumChanges(), 1);
// Start the fuchsia.memory.debug.MemoryPressure service.
SetupMemDebugService();
// Simulate pressure via the fuchsia.memory.debug.MemoryPressure service.
TestSimulatedPressure(fmp::Level::kCritical);
RunLoopUntilIdle();
// Verify that watchers saw the change.
ASSERT_EQ(watcher1.NumChanges(), 2);
ASSERT_EQ(watcher2.NumChanges(), 2);
TestSimulatedPressure(fmp::Level::kWarning);
RunLoopUntilIdle();
ASSERT_EQ(watcher1.NumChanges(), 3);
ASSERT_EQ(watcher2.NumChanges(), 3);
// Repeating the same level should count too.
TestSimulatedPressure(fmp::Level::kWarning);
RunLoopUntilIdle();
ASSERT_EQ(watcher1.NumChanges(), 4);
ASSERT_EQ(watcher2.NumChanges(), 4);
TestSimulatedPressure(fmp::Level::kNormal);
RunLoopUntilIdle();
ASSERT_EQ(watcher1.NumChanges(), 5);
ASSERT_EQ(watcher2.NumChanges(), 5);
// Verify that simulated signals don't affect the real signaling mechanism.
TriggerLevelChange(Level::kNormal);
RunLoopUntilIdle();
ASSERT_EQ(watcher1.NumChanges(), 6);
ASSERT_EQ(watcher2.NumChanges(), 6);
}
RunLoopUntilIdle();
ASSERT_EQ(GetWatcherCount(), 0ul);
}
} // namespace pressure_signaler::test