The fit keeps the slope and discards the intercept, so the result counts from the producer's boot. Tasks 4 and 7 compose it with ClockOffset::map, which is correct, but the bare name invites passing it straight to a plot axis. Also unwrapped the stalled-clock assertion from behind `if (fit.ready())` — that branch never runs, so the test confirmed nothing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
118 lines
4.3 KiB
C++
118 lines
4.3 KiB
C++
#include "TimeBase.h"
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
using namespace udpscope;
|
|
|
|
TEST(ClockOffset, MapsTheFirstReadingOntoWallClockExactly) {
|
|
ClockOffset off;
|
|
EXPECT_FALSE(off.valid());
|
|
|
|
const double wall = 1756291200.5;
|
|
EXPECT_DOUBLE_EQ(off.map(10.0, wall), wall);
|
|
EXPECT_TRUE(off.valid());
|
|
}
|
|
|
|
// Network delay jitters the arrival time. If the offset chased every packet
|
|
// the whole trace would wobble, so it is latched and only corrected on real
|
|
// drift.
|
|
TEST(ClockOffset, HoldsTheOffsetThroughSmallArrivalJitter) {
|
|
ClockOffset off;
|
|
off.map(10.0, 1000.0); // offset = 990
|
|
|
|
// Arrival wanders either side of the prediction. wallSec is a local receive
|
|
// timestamp, so it only ever advances — jitter shows up as the gap growing
|
|
// and shrinking, never as the clock going backwards.
|
|
EXPECT_DOUBLE_EQ(off.map(11.0, 1001.02), 1001.0); // +0.02 late
|
|
EXPECT_DOUBLE_EQ(off.map(12.0, 1001.97), 1002.0); // -0.03 early
|
|
}
|
|
|
|
// The threshold has to be symmetric. A producer whose clock steps FORWARD (an
|
|
// NTP correction on the producer's host, say) puts the prediction permanently
|
|
// ahead of the wall clock — a one-sided "recalibrate only when wall is ahead"
|
|
// test never fires for it, and the trace sits in the future for the rest of the
|
|
// run.
|
|
TEST(ClockOffset, RecalibratesWhenTheProducerClockJumpsForward) {
|
|
ClockOffset off;
|
|
off.map(10.0, 1000.0); // offset = 990
|
|
|
|
// Producer leaps 100 s ahead while only 1 s of wall time passes.
|
|
EXPECT_DOUBLE_EQ(off.map(111.0, 1001.0), 1001.0);
|
|
}
|
|
|
|
TEST(ClockOffset, RecalibratesWhenDriftExceedsTheThreshold) {
|
|
ClockOffset off;
|
|
off.map(10.0, 1000.0); // offset = 990
|
|
|
|
/* Producer clock jumped (restart, re-phase): 5 s of error is not jitter. */
|
|
const double mapped = off.map(11.0, 1006.0);
|
|
EXPECT_DOUBLE_EQ(mapped, 1006.0);
|
|
}
|
|
|
|
TEST(ClockOffset, ResetForgetsTheCalibration) {
|
|
ClockOffset off;
|
|
off.map(10.0, 1000.0);
|
|
off.reset();
|
|
EXPECT_FALSE(off.valid());
|
|
EXPECT_DOUBLE_EQ(off.map(50.0, 2000.0), 2000.0);
|
|
}
|
|
|
|
// The tick rate of the producer's high-resolution timer is not carried by the
|
|
// protocol, and StreamHub's trick of using the local MARTe timer frequency only
|
|
// works on the producer's own host. Recover it from the data instead.
|
|
TEST(HrtRateFit, RecoversAKnownTickRate) {
|
|
HrtRateFit fit;
|
|
const double ticksPerSec = 2.5e9;
|
|
|
|
EXPECT_FALSE(fit.ready());
|
|
for (int i = 0; i < 64; i++) {
|
|
const double wall = 1000.0 + i * 0.01;
|
|
fit.add(static_cast<uint64_t>(wall * ticksPerSec), wall);
|
|
}
|
|
|
|
ASSERT_TRUE(fit.ready());
|
|
EXPECT_NEAR(fit.ticksPerSecond(), ticksPerSec, ticksPerSec * 1e-6);
|
|
}
|
|
|
|
TEST(HrtRateFit, IsNotReadyBeforeTheMinimumSampleCount) {
|
|
HrtRateFit fit;
|
|
for (size_t i = 0; i < HrtRateFit::kMinSamples - 1; i++) {
|
|
fit.add(static_cast<uint64_t>(i) * 1000000u, 1000.0 + i * 0.001);
|
|
}
|
|
EXPECT_FALSE(fit.ready());
|
|
|
|
fit.add(static_cast<uint64_t>(HrtRateFit::kMinSamples) * 1000000u,
|
|
1000.0 + HrtRateFit::kMinSamples * 0.001);
|
|
EXPECT_TRUE(fit.ready());
|
|
}
|
|
|
|
TEST(HrtRateFit, ToSecondsUsesTheFittedRate) {
|
|
HrtRateFit fit;
|
|
const double ticksPerSec = 1.0e9;
|
|
for (int i = 0; i < 64; i++) {
|
|
const double wall = 500.0 + i * 0.005;
|
|
fit.add(static_cast<uint64_t>(wall * ticksPerSec), wall);
|
|
}
|
|
ASSERT_TRUE(fit.ready());
|
|
EXPECT_NEAR(fit.toSeconds(2000000000ull), 2.0, 1e-4);
|
|
}
|
|
|
|
TEST(HrtRateFit, SurvivesAStalledClock) {
|
|
HrtRateFit fit;
|
|
for (int i = 0; i < 64; i++) {
|
|
fit.add(12345u, 1000.0 + i * 0.01); /* hrt never advances */
|
|
}
|
|
/* A degenerate fit must not produce a rate that would divide by zero, so it
|
|
* must decline to be ready at all. Guarding this behind `if (fit.ready())`
|
|
* would make the test vacuous: the branch never runs and a fit that
|
|
* declared itself ready with a rate of 0 or NaN would pass unnoticed. */
|
|
EXPECT_FALSE(fit.ready());
|
|
EXPECT_DOUBLE_EQ(fit.ticksPerSecond(), 0.0);
|
|
}
|
|
|
|
TEST(TimeSignalScale, UsesNanosecondsForUint64AndMicrosecondsOtherwise) {
|
|
EXPECT_DOUBLE_EQ(TimeSignalScale(6 /* UDPS_T_UINT64 */), 1.0e-9);
|
|
EXPECT_DOUBLE_EQ(TimeSignalScale(9 /* UDPS_T_FLOAT64 */), 1.0e-6);
|
|
EXPECT_DOUBLE_EQ(TimeSignalScale(4 /* UDPS_T_UINT32 */), 1.0e-6);
|
|
}
|