blob: 3436a3ce089c7ccf77701d4132ea30bb5f164b65 [file]
/*
*
* Copyright (c) 2026 Project CHIP Authors
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "CommissioningProxyMockTimer.h"
#include <app/clusters/commissioning-proxy-server/CommissioningProxyBgScanRegistry.h>
#include <pw_unit_test/framework.h>
using namespace chip;
using namespace chip::app::Clusters::CommissioningProxy;
using chip::Protocols::InteractionModel::Status;
namespace {
constexpr FabricIndex kFabric1 = 1;
constexpr FabricIndex kFabric2 = 2;
constexpr NodeId kNode1 = 0x1111;
constexpr NodeId kNode2 = 0x2222;
constexpr NodeId kNode3 = 0x3333;
constexpr NodeId kNode4 = 0x4444;
constexpr NodeId kNode5 = 0x5555;
const BitMask<CapabilitiesBitmap> kBle{ CapabilitiesBitmap::kBle };
const BitMask<CapabilitiesBitmap> kPaf{ CapabilitiesBitmap::kWiFiPAF };
const BitMask<CapabilitiesBitmap> kNoTransport{};
const BitMask<WiFiBandBitmap> k2g4{ WiFiBandBitmap::k2g4 };
const BitMask<WiFiBandBitmap> k5g{ WiFiBandBitmap::k5g };
const BitMask<WiFiBandBitmap> kBoth{ WiFiBandBitmap::k2g4, WiFiBandBitmap::k5g };
const BitMask<WiFiBandBitmap> kNoBands{};
// Records the hardware calls the registry makes and lets a test program the
// StartHardwareScan() result (to model the BUSY-defer and hard-failure paths).
class MockHardwareControl : public CommissioningProxyBgScanRegistry::HardwareControl
{
public:
CHIP_ERROR StartHardwareScan() override
{
startCount++;
return startResult;
}
void StopHardwareScan() override { stopCount++; }
void ClearCachedResults(BitMask<WiFiBandBitmap> bands) override
{
clearCount++;
lastClearedBands = bands;
}
int startCount = 0;
int stopCount = 0;
int clearCount = 0;
BitMask<WiFiBandBitmap> lastClearedBands{};
CHIP_ERROR startResult = CHIP_NO_ERROR;
};
// Most tests pass timeoutSecs == 0 so no lifetime timer is armed and the registry
// logic is exercised synchronously. The lifetime tests at the end instead arm a timer
// and advance CommissioningProxyMockTimer's virtual clock.
constexpr System::Clock::Seconds16 kNoTimeout{ 0 };
// Every test drives one registry over one mock radio and one virtual clock. Declaration
// order matters: reg is destroyed first, so its Shutdown() still has both to talk to.
struct TestCommissioningProxyBgScanRegistry : public ::testing::Test
{
MockHardwareControl hw;
CommissioningProxyMockTimer timers;
CommissioningProxyBgScanRegistry reg{ hw, timers };
};
} // namespace
TEST_F(TestCommissioningProxyBgScanRegistry, FirstFabricStartsHardware)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(hw.startCount, 1);
EXPECT_FALSE(reg.IsEmpty());
EXPECT_FALSE(reg.IsPaused());
}
TEST_F(TestCommissioningProxyBgScanRegistry, SecondFabricDoesNotRestartHardware)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Start(kFabric2, kNode2, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(hw.startCount, 1); // already running; not restarted
}
TEST_F(TestCommissioningProxyBgScanRegistry, StartBusyStaysPausedAndKeepsFabric)
{
hw.startResult = CHIP_ERROR_BUSY; // radio held (BLE scanner / PAF connect slot)
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(hw.startCount, 1);
EXPECT_TRUE(reg.IsPaused());
EXPECT_FALSE(reg.IsEmpty()); // fabric kept, deferred
// Radio frees up: resume restarts the hardware scan.
hw.startResult = CHIP_NO_ERROR;
reg.ResumeIfNeeded();
EXPECT_EQ(hw.startCount, 2);
EXPECT_FALSE(reg.IsPaused());
}
TEST_F(TestCommissioningProxyBgScanRegistry, StartHardFailureRejectsAndDropsFabric)
{
hw.startResult = CHIP_ERROR_INTERNAL;
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Failure);
EXPECT_TRUE(reg.IsEmpty());
// Nothing was started, so the radio is not stopped and the cache is not cleared.
EXPECT_EQ(hw.stopCount, 0);
EXPECT_EQ(hw.clearCount, 0);
}
// A failed Start SHALL be a no-op: a hard hardware failure while refreshing an
// existing fabric must not destroy the registration that was already working.
TEST_F(TestCommissioningProxyBgScanRegistry, StartHardFailureOnRefreshKeepsExistingRegistration)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
// The radio is taken for a connect: the registry pauses (one stop so far).
reg.Pause();
EXPECT_TRUE(reg.IsPaused());
EXPECT_EQ(hw.stopCount, 1);
// Refreshing that same fabric now fails hard.
hw.startResult = CHIP_ERROR_INTERNAL;
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k5g, kNoTimeout), Status::Failure);
// The previous registration survives, still paused and awaiting a resume; nothing
// extra was stopped and no cached results were thrown away.
EXPECT_FALSE(reg.IsEmpty());
EXPECT_TRUE(reg.IsPaused());
EXPECT_EQ(hw.stopCount, 1);
EXPECT_EQ(hw.clearCount, 0);
// The old bands are intact: a stop for 2G4 still overlaps.
hw.startResult = CHIP_NO_ERROR;
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kNoTransport, k2g4), Status::Success);
EXPECT_TRUE(reg.IsEmpty());
}
TEST_F(TestCommissioningProxyBgScanRegistry, StopLastFabricStopsHardwareAndClears)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kBle, kNoBands, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kBle, kNoBands), Status::Success);
EXPECT_TRUE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 1);
EXPECT_EQ(hw.clearCount, 1);
}
TEST_F(TestCommissioningProxyBgScanRegistry, StopUnknownFabricNotFound)
{
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kBle, kNoBands), Status::NotFound);
}
TEST_F(TestCommissioningProxyBgScanRegistry, StopNoOverlapSucceedsAndKeepsFabric)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
// Stop targets a different transport (BLE); nothing overlaps this PAF fabric.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kBle, kNoBands), Status::Success);
EXPECT_FALSE(reg.IsEmpty()); // fabric untouched
EXPECT_EQ(hw.stopCount, 0);
}
// Also covers the whole-transport clear: once the last band goes the transport itself
// stops, so everything cached on it goes (bands == 0), not just the band named.
TEST_F(TestCommissioningProxyBgScanRegistry, StopBandSubsetKeepsFabricThenRemoves)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, kBoth, kNoTimeout), Status::Success);
// transport==0 means "stop only the given bands": drop 2G4, keep 5G. Nobody scans
// 2G4 any more, so its cached results go while the radio keeps running for 5G.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kNoTransport, k2g4), Status::Success);
EXPECT_FALSE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 0);
EXPECT_EQ(hw.clearCount, 1);
EXPECT_EQ(hw.lastClearedBands.Raw(), k2g4.Raw());
// Stopping the remaining band empties the fabric and tears the whole transport down.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kNoTransport, k5g), Status::Success);
EXPECT_TRUE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 1);
EXPECT_EQ(hw.clearCount, 2);
EXPECT_EQ(hw.lastClearedBands.Raw(), 0u);
}
TEST_F(TestCommissioningProxyBgScanRegistry, StopWithOtherFabricCoveringKeepsHardware)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Start(kFabric2, kNode2, kPaf, k2g4, kNoTimeout), Status::Success);
// Removing fabric 1 leaves fabric 2 still scanning PAF/2G4 — hardware stays up.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kPaf, k2g4), Status::Success);
EXPECT_FALSE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 0);
EXPECT_EQ(hw.clearCount, 0);
}
TEST_F(TestCommissioningProxyBgScanRegistry, PauseStopsAndResumeRestarts)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kBle, kNoBands, kNoTimeout), Status::Success);
EXPECT_EQ(hw.startCount, 1);
reg.Pause();
EXPECT_TRUE(reg.IsPaused());
EXPECT_EQ(hw.stopCount, 1);
reg.ResumeIfNeeded();
EXPECT_FALSE(reg.IsPaused());
EXPECT_EQ(hw.startCount, 2);
}
TEST_F(TestCommissioningProxyBgScanRegistry, PauseWhenEmptyIsNoop)
{
reg.Pause();
EXPECT_FALSE(reg.IsPaused());
EXPECT_EQ(hw.stopCount, 0);
}
TEST_F(TestCommissioningProxyBgScanRegistry, ResumeWhenNotPausedIsNoop)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kBle, kNoBands, kNoTimeout), Status::Success);
reg.ResumeIfNeeded();
EXPECT_EQ(hw.startCount, 1); // was already running
}
TEST_F(TestCommissioningProxyBgScanRegistry, ResumeStillBusyStaysPaused)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
reg.Pause();
EXPECT_TRUE(reg.IsPaused());
hw.startResult = CHIP_ERROR_BUSY; // radio still held on the resume attempt
reg.ResumeIfNeeded();
EXPECT_TRUE(reg.IsPaused()); // stays paused, will retry later
}
TEST_F(TestCommissioningProxyBgScanRegistry, ShutdownStopsHardware)
{
// Its own registry, not the fixture's: this needs to observe the destructor, so the
// lifetime has to end inside the test body. The fixture's own reg holds no fabric, so
// its later teardown cannot touch stopCount.
{
CommissioningProxyBgScanRegistry scoped(hw, timers);
EXPECT_EQ(scoped.Start(kFabric1, kNode1, kBle, kNoBands, kNoTimeout), Status::Success);
scoped.Shutdown();
EXPECT_TRUE(scoped.IsEmpty());
EXPECT_EQ(hw.stopCount, 1);
}
// Destructor runs Shutdown() again on the now-empty registry: no extra stop.
EXPECT_EQ(hw.stopCount, 1);
}
TEST_F(TestCommissioningProxyBgScanRegistry, RefreshExistingFabricUpdatesBands)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
// Re-register the same fabric with a different band; still one fabric, no restart.
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k5g, kNoTimeout), Status::Success);
EXPECT_EQ(hw.startCount, 1);
// A stop for the OLD band (2G4) no longer overlaps — proves the bands were updated.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kNoTransport, k2g4), Status::Success);
EXPECT_FALSE(reg.IsEmpty());
// Stopping the current band (5G) removes it.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kNoTransport, k5g), Status::Success);
EXPECT_TRUE(reg.IsEmpty());
}
// Spec § ProxyBackGroundScanStartRequest Timeout: "The background scan is
// automatically stopped when this duration elapses."
TEST_F(TestCommissioningProxyBgScanRegistry, LifetimeExpiryRemovesFabricAndStopsHardware)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, System::Clock::Seconds16(30)), Status::Success);
EXPECT_EQ(hw.startCount, 1);
// Still inside the lifetime: nothing changes.
timers.AdvanceClock(System::Clock::Seconds16(29));
EXPECT_FALSE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 0);
// Spec: "any cached results for that fabric's transports are cleared".
timers.AdvanceClock(System::Clock::Seconds16(1));
EXPECT_TRUE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 1);
EXPECT_EQ(hw.clearCount, 1);
}
// Without a lifetime timer the hardware scan would run unbounded, so a StartTimer
// failure SHALL reject the request and tear down the scan it had already started.
TEST_F(TestCommissioningProxyBgScanRegistry, LifetimeTimerArmFailureRejectsAndCleansUp)
{
timers.FailNextStart();
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, System::Clock::Seconds16(30)), Status::Failure);
EXPECT_TRUE(reg.IsEmpty());
EXPECT_FALSE(reg.IsPaused());
EXPECT_EQ(hw.startCount, 1); // hardware was started before the timer failed
EXPECT_EQ(hw.stopCount, 1); // ... so it must be stopped again
EXPECT_EQ(hw.clearCount, 1);
}
// Refreshing a node's request cancels the fabric's lifetime timer so it can be re-armed
// at the new deadline. If the re-arm fails the old timer is already gone, so keeping the
// fabric registered would scan unbounded — the fabric SHALL be released instead.
TEST_F(TestCommissioningProxyBgScanRegistry, RefreshTimerArmFailureReleasesTheFabric)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, System::Clock::Seconds16(30)), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode2, kPaf, k5g, System::Clock::Seconds16(60)), Status::Success);
EXPECT_EQ(hw.startCount, 1);
// Node 1 refreshes its own request with a new timeout; re-arming the fabric's
// lifetime fails, so the fabric goes — node 2's request included.
timers.FailNextStart();
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, System::Clock::Seconds16(45)), Status::Failure);
EXPECT_TRUE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 1); // nothing is left to scan for
EXPECT_EQ(hw.clearCount, 1);
// No timer survived the release: running past every deadline changes nothing.
timers.AdvanceClock(System::Clock::Seconds16(120));
EXPECT_TRUE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 1);
// The freed slot carries no request into the next fabric to claim it.
EXPECT_EQ(reg.Start(kFabric2, kNode3, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Stop(kFabric2, kNode2, kPaf, k5g), Status::NotFound);
}
// Releasing a fabric on a re-arm failure must not disturb another fabric's scan; only the
// bands the released requests were the last to want stop being cached.
TEST_F(TestCommissioningProxyBgScanRegistry, RefreshTimerArmFailureKeepsOtherFabricScanning)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k5g, System::Clock::Seconds16(30)), Status::Success);
EXPECT_EQ(reg.Start(kFabric2, kNode2, kPaf, k2g4, kNoTimeout), Status::Success);
timers.FailNextStart();
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k5g, System::Clock::Seconds16(45)), Status::Failure);
EXPECT_FALSE(reg.IsEmpty()); // fabric 2 still wants the radio
EXPECT_EQ(hw.stopCount, 0);
EXPECT_EQ(hw.clearCount, 1);
EXPECT_EQ(hw.lastClearedBands.Raw(), k5g.Raw()); // fabric 2 still scans 2G4
}
// A fabric released because its lifetime could not be re-armed takes its surviving
// requests' bands with it, so their cached results are dropped too — even when another
// fabric keeps the radio running.
TEST_F(TestCommissioningProxyBgScanRegistry, ReArmFailureOnStopClearsTheReleasedFabricsBands)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, System::Clock::Seconds16(30)), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode2, kPaf, k5g, System::Clock::Seconds16(60)), Status::Success);
EXPECT_EQ(reg.Start(kFabric2, kNode3, kPaf, k2g4, kNoTimeout), Status::Success);
// Node 1 stopping shortens fabric 1 to node 2's deadline; the re-arm fails, so
// fabric 1 is released and node 2's 5G results are no longer wanted by anyone.
timers.FailNextStart();
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kPaf, k2g4), Status::Success);
EXPECT_FALSE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 0);
EXPECT_EQ(hw.lastClearedBands.Raw(), k5g.Raw());
}
// Dropping a fabric because its lifetime could not be re-armed must take the fabric's
// surviving requests with it: the next fabric to occupy the freed slot cannot inherit
// requests it never made.
TEST_F(TestCommissioningProxyBgScanRegistry, LifetimeReArmFailureDropsTheFabricsRequests)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, System::Clock::Seconds16(30)), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode2, kPaf, k5g, System::Clock::Seconds16(60)), Status::Success);
// Node 1 stopping shortens the fabric to node 2's deadline, so the lifetime is
// re-armed — and that failing drops the fabric, node 2's request included.
timers.FailNextStart();
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kPaf, k2g4), Status::Success);
EXPECT_TRUE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 1);
// Another fabric takes the freed slot; node 2's request is not part of it.
EXPECT_EQ(reg.Start(kFabric2, kNode3, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Stop(kFabric2, kNode2, kPaf, k5g), Status::NotFound);
EXPECT_EQ(reg.Stop(kFabric2, kNode3, kPaf, k2g4), Status::Success);
EXPECT_TRUE(reg.IsEmpty());
}
// Spec: the proxy keeps per-fabric records and scans "until the timeout fires or
// ProxyBackGroundScanStopRequest is received for that fabric". Each fabric's lifetime
// is therefore independent: expiring one must not disturb another.
TEST_F(TestCommissioningProxyBgScanRegistry, IndependentLifetimesExpireSeparately)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, System::Clock::Seconds16(10)), Status::Success);
EXPECT_EQ(reg.Start(kFabric2, kNode2, kPaf, k2g4, System::Clock::Seconds16(60)), Status::Success);
EXPECT_EQ(hw.startCount, 1);
// Fabric 1 expires; fabric 2 still wants the scan, so the radio keeps running.
timers.AdvanceClock(System::Clock::Seconds16(10));
EXPECT_FALSE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 0);
EXPECT_EQ(hw.clearCount, 0);
// Fabric 2 expires: last fabric gone, scan torn down.
timers.AdvanceClock(System::Clock::Seconds16(50));
EXPECT_TRUE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 1);
EXPECT_EQ(hw.clearCount, 1);
}
// Spec § ProxyBackGroundScanStopRequest: "If the NodeID and FabricID of the client do
// not match those recorded when background scanning was started for this fabric, the
// proxy SHALL take no action and the command SHALL be rejected with a status of
// NOT_FOUND."
TEST_F(TestCommissioningProxyBgScanRegistry, StopFromNodeWithNoRequestNotFound)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
// Node 2 never started a scan, so it has no request to stop.
EXPECT_EQ(reg.Stop(kFabric1, kNode2, kPaf, k2g4), Status::NotFound);
EXPECT_FALSE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 0);
// The owner still can.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kPaf, k2g4), Status::Success);
EXPECT_TRUE(reg.IsEmpty());
}
// Spec: a Stop is identified by NodeID + FabricID, so each node on a fabric keeps its
// own request and the fabric scans the union of them. One node's scan is untouched by
// another's.
TEST_F(TestCommissioningProxyBgScanRegistry, SecondNodeOnSameFabricAddsItsOwnRequest)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode2, kPaf, k5g, kNoTimeout), Status::Success);
EXPECT_EQ(hw.startCount, 1); // already running; not restarted for the second
// Node 1 stopping its own 2G4 leaves node 2's 5G request scanning: the radio stays
// on, but 2G4's cached results go since no request covers that band now.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kNoTransport, k2g4), Status::Success);
EXPECT_FALSE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 0);
EXPECT_EQ(hw.clearCount, 1);
EXPECT_EQ(hw.lastClearedBands.Raw(), k2g4.Raw());
// Only once the last request goes does the radio stop and the transport clear.
EXPECT_EQ(reg.Stop(kFabric1, kNode2, kNoTransport, k5g), Status::Success);
EXPECT_TRUE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 1);
EXPECT_EQ(hw.clearCount, 2);
}
// Spec § ProxyBackGroundScanStopRequest: stop the requested transports and bands
// "unless another active background scan overlaps with them".
TEST_F(TestCommissioningProxyBgScanRegistry, StopLeavesBandsAnotherRequestStillCovers)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, kBoth, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode2, kPaf, k5g, kNoTimeout), Status::Success);
// Node 1 drops 5G; node 2 still wants it, so scanning continues.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kNoTransport, k5g), Status::Success);
EXPECT_FALSE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 0);
// Node 1 keeps 2G4: dropping that too removes only node 1's request.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kNoTransport, k2g4), Status::Success);
EXPECT_FALSE(reg.IsEmpty());
EXPECT_EQ(reg.Stop(kFabric1, kNode2, kNoTransport, k5g), Status::Success);
EXPECT_TRUE(reg.IsEmpty());
}
// The fabric holds one timer, at the latest deadline of its requests, so a shorter
// request does not end the fabric's scan early.
TEST_F(TestCommissioningProxyBgScanRegistry, FabricTimerUsesLatestDeadline)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, System::Clock::Seconds16(10)), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode2, kPaf, k5g, System::Clock::Seconds16(60)), Status::Success);
timers.AdvanceClock(System::Clock::Seconds16(11));
EXPECT_FALSE(reg.IsEmpty()); // node 2 still has 49s to run
timers.AdvanceClock(System::Clock::Seconds16(49));
EXPECT_TRUE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 1);
}
// Dropping the request that set the fabric's deadline SHALL pull the timer back to the
// longest survivor, rather than leave the fabric scanning until the removed deadline.
TEST_F(TestCommissioningProxyBgScanRegistry, StopShortensFabricTimerToSurvivingRequest)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, System::Clock::Seconds16(300)), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode2, kPaf, k5g, System::Clock::Seconds16(30)), Status::Success);
// The 300s request goes; only the 30s one is left.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kNoTransport, k2g4), Status::Success);
EXPECT_FALSE(reg.IsEmpty());
timers.AdvanceClock(System::Clock::Seconds16(31));
EXPECT_TRUE(reg.IsEmpty());
}
// A request with Timeout 0 never expires, so the fabric holds no timer at all while it
// is registered.
TEST_F(TestCommissioningProxyBgScanRegistry, NoTimeoutRequestSuppressesFabricTimer)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, System::Clock::Seconds16(30)), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode2, kPaf, k5g, kNoTimeout), Status::Success);
EXPECT_EQ(timers.ActiveCount(), 0u);
timers.AdvanceClock(System::Clock::Seconds16(60));
EXPECT_FALSE(reg.IsEmpty());
}
// A fabric may only hold so many concurrent requests; the cap does not stop a node that
// already has one from refreshing it.
TEST_F(TestCommissioningProxyBgScanRegistry, RequestLimitPerFabricRejectsExtraNodes)
{
static_assert(CHIP_CONFIG_COMMISSIONING_PROXY_MAX_BGSCAN_REQUESTS_PER_FABRIC == 4, "test assumes a cap of 4");
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode2, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode3, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode4, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode5, kPaf, k2g4, kNoTimeout), Status::ResourceExhausted);
// An existing node refreshing is not a new request.
EXPECT_EQ(reg.Start(kFabric1, kNode2, kPaf, k5g, kNoTimeout), Status::Success);
// The cap is per fabric, so another fabric is unaffected.
EXPECT_EQ(reg.Start(kFabric2, kNode5, kPaf, k2g4, kNoTimeout), Status::Success);
}
// Spec § ProxyBackGroundScanStopRequest: "clear all cached results for the transports
// and bands on which it has stopped scanning" — so a band another fabric still scans
// keeps its results.
TEST_F(TestCommissioningProxyBgScanRegistry, StopClearsOnlyBandsNoFabricStillScans)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, kBoth, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Start(kFabric2, kNode2, kPaf, k5g, kNoTimeout), Status::Success);
// Fabric 1 drops both bands. 5G survives on fabric 2, so only 2G4's results go.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kNoTransport, kBoth), Status::Success);
EXPECT_EQ(hw.clearCount, 1);
EXPECT_EQ(hw.lastClearedBands.Raw(), k2g4.Raw());
EXPECT_EQ(hw.stopCount, 0); // fabric 2 still wants the radio
}
// A band that another fabric still scans SHALL not have its results cleared at all.
TEST_F(TestCommissioningProxyBgScanRegistry, StopOfSharedBandClearsNothing)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Start(kFabric2, kNode2, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kNoTransport, k2g4), Status::Success);
EXPECT_EQ(hw.clearCount, 0); // fabric 2 still scans 2G4
EXPECT_FALSE(reg.IsEmpty());
}
// Spec § ProxyBackGroundScanStartRequest: "When the per-fabric Timeout elapses ... any
// cached results for that fabric's transports are cleared" — again only for bands no
// surviving fabric covers.
TEST_F(TestCommissioningProxyBgScanRegistry, LifetimeExpiryClearsOnlyItsOwnBands)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, System::Clock::Seconds16(30)), Status::Success);
EXPECT_EQ(reg.Start(kFabric2, kNode2, kPaf, k5g, kNoTimeout), Status::Success);
timers.AdvanceClock(System::Clock::Seconds16(31));
EXPECT_FALSE(reg.IsEmpty()); // fabric 2 has no timeout
EXPECT_EQ(hw.clearCount, 1);
EXPECT_EQ(hw.lastClearedBands.Raw(), k2g4.Raw());
EXPECT_EQ(hw.stopCount, 0);
}
// FabricIndex values are reused after a fabric is removed, so a Timeout-0 request —
// which has no timer and would otherwise never end — must not outlive its fabric.
TEST_F(TestCommissioningProxyBgScanRegistry, RemoveFabricDropsItsRequestsAndStopsHardware)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Start(kFabric1, kNode2, kPaf, k5g, kNoTimeout), Status::Success);
reg.RemoveFabric(kFabric1);
EXPECT_TRUE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 1);
EXPECT_EQ(hw.clearCount, 1);
EXPECT_EQ(hw.lastClearedBands.Raw(), 0u); // whole transport, nothing left scanning
// The reused index starts clean: a stop from the old node finds nothing.
EXPECT_EQ(reg.Stop(kFabric1, kNode1, kNoTransport, k2g4), Status::NotFound);
}
// Removing one fabric SHALL leave another fabric's scan running, clearing only the
// bands that nobody covers any more.
TEST_F(TestCommissioningProxyBgScanRegistry, RemoveFabricLeavesOtherFabricsScanning)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
EXPECT_EQ(reg.Start(kFabric2, kNode2, kPaf, k5g, kNoTimeout), Status::Success);
reg.RemoveFabric(kFabric1);
EXPECT_FALSE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 0);
EXPECT_EQ(hw.clearCount, 1);
EXPECT_EQ(hw.lastClearedBands.Raw(), k2g4.Raw());
}
// Removing a fabric with no requests is a no-op.
TEST_F(TestCommissioningProxyBgScanRegistry, RemoveUnknownFabricIsNoop)
{
EXPECT_EQ(reg.Start(kFabric1, kNode1, kPaf, k2g4, kNoTimeout), Status::Success);
reg.RemoveFabric(kFabric2);
EXPECT_FALSE(reg.IsEmpty());
EXPECT_EQ(hw.stopCount, 0);
EXPECT_EQ(hw.clearCount, 0);
}