/** * @file AccumDtGTest.cpp * @brief Tests UDPSEstimateAccumDt, the per-sample period estimator used for * accumulated scalars that carry no SamplingRate. * * The estimator exists because the natural formula — sender-clock gap divided * by the previous packet's sample count — is only correct while no packet is * lost. When one is, the gap covers cycles that count never saw and the period * comes out too large, which spreads the packet's samples past their real end * and into the range the next packet claims. The loss count comes from the * producer's packet counter rather than being inferred from the gap itself, so * these tests pin both sides: the estimate must not move when packets go * missing, and it must still follow a genuine rate change — a cycle count * inferred from the estimate's own period would lock onto the old one. * * @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and * the Development of Fusion Energy ('Fusion for Energy'). * Licensed under the EUPL, Version 1.1 or - as soon they will be approved * by the European Commission - subsequent versions of the EUPL (the "Licence") * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl * * @warning Unless required by applicable law or agreed to in writing, * software distributed under the Licence is distributed on an "AS IS" * basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the Licence permissions and limitations under the Licence. */ #include #include "UDPSourceSession.h" using MARTe::float64; using MARTe::uint32; using StreamHub::UDPSEstimateAccumDt; namespace { /** A producer emitting batches of BATCH cycles at a period of DT seconds. */ const float64 kDt = 1.0e-3; const uint32 kBatch = 10u; const float64 kGap = kDt * static_cast(kBatch); /** Feeds n clean packets and returns the settled estimate. */ float64 Warmup(uint32 n, float64 &dtEMA, bool &dtValid) { float64 dt = 0.0; for (uint32 i = 0u; i < n; i++) { dt = UDPSEstimateAccumDt(kGap, kBatch, 0u, dtEMA, dtValid); } return dt; } } // namespace /* The first packet has nothing to go on but the previous sample count, so it * must fall back to gap/prevN rather than to some fixed default. */ TEST(AccumDtGTest, BootstrapsFromPreviousSampleCount) { float64 dtEMA = 0.0; bool dtValid = false; const float64 dt = UDPSEstimateAccumDt(kGap, kBatch, 0u, dtEMA, dtValid); EXPECT_TRUE(dtValid); EXPECT_NEAR(kDt, dt, 1.0e-12); } /* A clean stream must hold the period steady, not drift. */ TEST(AccumDtGTest, SteadyStreamStaysOnPeriod) { float64 dtEMA = 0.0; bool dtValid = false; const float64 dt = Warmup(50u, dtEMA, dtValid); EXPECT_NEAR(kDt, dt, 1.0e-9); } /* The regression this whole estimator is for: one packet is lost, so the gap * doubles while prevN does not. Dividing by prevN would report 2x the true * period — enough to walk a 10-sample batch a full batch past its own end. */ TEST(AccumDtGTest, LostPacketDoesNotInflatePeriod) { float64 dtEMA = 0.0; bool dtValid = false; (void) Warmup(50u, dtEMA, dtValid); const float64 dt = UDPSEstimateAccumDt(2.0 * kGap, kBatch, 1u, dtEMA, dtValid); /* What the naive formula would have produced. */ const float64 naive = (2.0 * kGap) / static_cast(kBatch); EXPECT_NEAR(2.0 * kDt, naive, 1.0e-12); EXPECT_NEAR(kDt, dt, 1.0e-6); } /* Several consecutive losses are the same situation, just wider. */ TEST(AccumDtGTest, MultiplePacketLossDoesNotInflatePeriod) { float64 dtEMA = 0.0; bool dtValid = false; (void) Warmup(50u, dtEMA, dtValid); for (uint32 missing = 1u; missing <= 5u; missing++) { const float64 span = static_cast(missing + 1u) * kGap; const float64 dt = UDPSEstimateAccumDt(span, kBatch, missing, dtEMA, dtValid); EXPECT_NEAR(kDt, dt, 1.0e-6) << "after " << missing << " lost packet(s)"; } } /* Loss must not leave the estimator poisoned for the packets that follow. */ TEST(AccumDtGTest, RecoversToCleanStreamAfterLoss) { float64 dtEMA = 0.0; bool dtValid = false; (void) Warmup(50u, dtEMA, dtValid); (void) UDPSEstimateAccumDt(3.0 * kGap, kBatch, 2u, dtEMA, dtValid); const float64 dt = Warmup(20u, dtEMA, dtValid); EXPECT_NEAR(kDt, dt, 1.0e-6); } /* A real, sustained rate change must still be followed — the estimator is a * smoother, not a latch. Half the period is exactly on the rejection boundary, * so use a change that lands inside the accepted band. */ TEST(AccumDtGTest, FollowsSustainedRateChange) { float64 dtEMA = 0.0; bool dtValid = false; (void) Warmup(50u, dtEMA, dtValid); const float64 newDt = kDt * 0.75; const float64 newGap = newDt * static_cast(kBatch); float64 dt = 0.0; for (uint32 i = 0u; i < 400u; i++) { dt = UDPSEstimateAccumDt(newGap, kBatch, 0u, dtEMA, dtValid); } EXPECT_NEAR(newDt, dt, 1.0e-6); } /* A batch that carries fewer cycles than usual (a time-triggered flush) is not * loss: the gap shrinks with it, so the period must not shrink too. */ TEST(AccumDtGTest, ShortBatchDoesNotDeflatePeriod) { float64 dtEMA = 0.0; bool dtValid = false; (void) Warmup(50u, dtEMA, dtValid); const uint32 shortBatch = 3u; const float64 dt = UDPSEstimateAccumDt( kDt * static_cast(shortBatch), shortBatch, 0u, dtEMA, dtValid); EXPECT_NEAR(kDt, dt, 1.0e-6); } /* A gap shorter than one period cannot mean zero cycles; the divisor is * clamped so the estimate can never be driven to infinity. */ TEST(AccumDtGTest, SubPeriodGapDoesNotExplode) { float64 dtEMA = 0.0; bool dtValid = false; (void) Warmup(50u, dtEMA, dtValid); const float64 dt = UDPSEstimateAccumDt(kDt * 1.0e-3, 1u, 0u, dtEMA, dtValid); EXPECT_NEAR(kDt, dt, 1.0e-6); }