Files
MARTe-Integrated-Components/Test/Components/DataSources/UDPStreamer/UDPStreamerTest.h
T
Martino FerrariandClaude Opus 4.6 fbae7d712c fix(udps): stop packets being dated from an earlier time base
Reported as samples sporadically carrying a previous packet's timestamp:
holes on one side of the stream and collisions on the other, in both the
Go and the MARTe2 receiver. That it appeared in both is what located it
-- the shared cause is upstream of either client. Four independent
defects, all of which end in a packet's values being placed at a time
that is not theirs.

Reassembly slot exhaustion (the "Reassembly slots full; evicting oldest"
flood). Chunk size was learnt only from fragment 0, so an out-of-order
burst destroyed a packet whose bytes had all arrived and left the slot
occupied until the 2 s GC. Slots were keyed on the counter alone, but
DATA and CONFIG number independently, so equal counters merged the two
streams. The 32-byte received-mask covered 256 of the 512 fragments the
client accepts, so a duplicate above 255 was counted as new and the
packet was delivered with a hole of stale bytes in it. And one datagram
was read per Execute(), which cannot drain a fast producer. Fixed with a
pendingTail deferral, (counter, type) keying, a 64-byte mask, a
256-datagram drain, counter-age slot reclamation, and a 1 Hz aggregated
warning in place of the per-eviction flood.

UDPStreamer dropping whole Accumulate batches. EventSem::ResetWait is
Reset-then-Wait, so a Post() landing while the sender thread was inside
ServiceClients()/SendData() was destroyed by the next Reset. The batch
was then skipped with dataReady false, readyFill was never cleared, and
the following flush overwrote it: an entire run of RT cycles never
reached the wire. The record of pending work now lives in the buffers
rather than in the semaphore edge, which also removes up to
UDPS_DATA_WAIT_MS of latency; genuine backpressure overwrites are
counted and reported. Against the unfixed code the new test sees
2999/3000 batches never consumed.

Period inflation after loss. Accumulated scalars carry no SamplingRate,
so the receiver derives dt from the sender-clock gap -- but dividing it
by the previous packet's sample count is only right while nothing is
lost. One loss doubles the reported period, which spreads a batch a full
batch past its own end and into the range the next packet claims. That
is the hole and the collision, exactly. Inferring the cycle count from
the estimate's own period is not a way out: it has a stable fixed point
wherever gap/dt is an integer, so a real rate change locks it at the old
one for good (AccumDtGTest.FollowsSustainedRateChange).

The packet counter removes the ambiguity, so all three receivers now
order on it: a DATA packet that does not advance the counter is dropped
rather than delivered, because its values are older than data already
handed over. Ordering is on the signed difference so it survives the
uint32 wrap, and the sequence resets on reconnect, where the producer's
counter restarts independently of ours. The loss count that falls out of
the same delta feeds the period estimate as cycles = prevN * (1 + lost),
which reduces exactly to gap/prevN when nothing is lost and therefore
still tracks a genuine rate change. UDPSClient::AcceptDataCounter (C++),
udpsprotocol.SequenceGate (Go), decode_data (C).

The C client's existing gap counter was wrap-unsafe and let a stale
packet rewind last_counter, which made every subsequent gap wrong; it
uses the same code now. Docs/Protocol.md gains an Ordering DATA section
stating the requirement for any receiver, including ones outside this
repository.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-02 01:18:49 +02:00

250 lines
7.8 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file UDPStreamerTest.h
* @brief Header file for class UDPStreamerTest
* @date 13/05/2026
* @author Martino Ferrari
*
* @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.
*
* @details This header file contains the declaration of the class UDPStreamerTest
* with all of its public, protected and private members. It may also include
* definitions for inline methods which need to be visible to the compiler.
*/
#ifndef UDPSTREAMERTEST_H_
#define UDPSTREAMERTEST_H_
/*---------------------------------------------------------------------------*/
/* Standard header includes */
/*---------------------------------------------------------------------------*/
/*---------------------------------------------------------------------------*/
/* Project header includes */
/*---------------------------------------------------------------------------*/
/*---------------------------------------------------------------------------*/
/* Class declaration */
/*---------------------------------------------------------------------------*/
/**
* @brief Tests the UDPStreamer DataSource public methods.
*/
class UDPStreamerTest {
public:
/**
* @brief Tests the default constructor.
*/
bool TestConstructor();
/**
* @brief Tests Initialise with a fully valid configuration.
*/
bool TestInitialise_Valid();
/**
* @brief Tests that a missing Port uses the default (44500).
*/
bool TestInitialise_DefaultPort();
/**
* @brief Tests that MaxPayloadSize below the minimum is rejected.
*/
bool TestInitialise_InvalidMaxPayloadSize();
/**
* @brief Tests that StackSize = 0 is rejected.
*/
bool TestInitialise_ZeroStackSize();
/**
* @brief Tests GetBrokerName returns the correct broker for OutputSignals.
*/
bool TestGetBrokerName_Output();
/**
* @brief Tests GetBrokerName returns empty string for InputSignals.
*/
bool TestGetBrokerName_Input();
/**
* @brief Tests AllocateMemory allocates readyBuffer, scratchBuffer, wireBuffer.
*/
bool TestAllocateMemory();
/**
* @brief Tests SetConfiguredDatabase with basic signal types.
*/
bool TestSetConfiguredDatabase_BasicSignals();
/**
* @brief Tests SetConfiguredDatabase with a quantized float signal.
*/
bool TestSetConfiguredDatabase_Quantized();
/**
* @brief Tests that quantization on a non-float signal is rejected.
*/
bool TestSetConfiguredDatabase_InvalidQuantOnInteger();
/**
* @brief Tests that an unknown QuantizedType is rejected.
*/
bool TestSetConfiguredDatabase_UnknownQuantType();
/**
* @brief Tests TimeMode = PacketTime (default).
*/
bool TestSetConfiguredDatabase_TimeModePacket();
/**
* @brief Tests TimeMode = FullArray with a matching time signal.
*/
bool TestSetConfiguredDatabase_TimeModeFullArray();
/**
* @brief Tests TimeMode = FirstSample with a scalar time signal.
*/
bool TestSetConfiguredDatabase_TimeModeFirstSample();
/**
* @brief Tests that FirstSample without SamplingRate is rejected.
*/
bool TestSetConfiguredDatabase_MissingSamplingRate();
/**
* @brief Tests that TimeSignal pointing to a non-existent signal is rejected.
*/
bool TestSetConfiguredDatabase_InvalidTimeSignal();
/**
* @brief Tests that FullArray TimeMode with mismatched element counts is rejected.
*/
bool TestSetConfiguredDatabase_FullArrayMismatch();
/**
* @brief Tests PrepareNextState starts the thread and opens the socket.
*/
bool TestPrepareNextState();
/**
* @brief Tests Synchronise when no client is connected (must return true, no crash).
*/
bool TestSynchronise_NoClient();
/**
* @brief Tests the full CONNECT → CONFIG → DATA → DISCONNECT flow via loopback UDP.
*/
bool TestExecute_ConnectDataDisconnect();
/**
* @brief Tests that DATA packets are fragmented correctly for large payloads.
*/
bool TestExecute_Fragmentation();
/**
* @brief Tests uint16 quantization: verifies that 0 maps to 0 and max maps to 65535.
*/
bool TestQuantization_Uint16Extremes();
/**
* @brief Tests uint8 quantization: verifies clamping of out-of-range values.
*/
bool TestQuantization_Uint8Clamping();
/**
* @brief Tests PublishingMode = "Auto" with a valid MinRefreshRate.
*/
bool TestInitialise_AutoMode_Valid();
/**
* @brief Tests that PublishingMode = "Auto" without MinRefreshRate is rejected.
*/
bool TestInitialise_AutoMode_MissingRefreshRate();
/**
* @brief Tests that an unknown PublishingMode string is rejected.
*/
bool TestInitialise_UnknownPublishingMode();
/**
* @brief Tests the GetPort() accessor.
*/
bool TestGetPort();
/**
* @brief Tests the GetMaxPayloadSize() accessor.
*/
bool TestGetMaxPayloadSize();
/**
* @brief Tests that IsClientConnected() returns false before any CONNECT.
*/
bool TestIsClientConnected_InitiallyFalse();
/**
* @brief Tests AllocateMemory with 4 high-frequency int16[1000] packed channels (1 MSps).
*/
bool TestHighFrequency_AllocateMemory();
/**
* @brief Tests that 4 int16[1000] channels produce exactly 6 fragments at MaxPayloadSize=1400.
* Payload: 8 (HRT) + 8 (T0/uint64) + 4×2000 (int16[1000]) = 8016 bytes;
* chunk = 1383 bytes → ceil(8016/1383) = 6 fragments.
*/
bool TestHighFrequency_Fragmentation();
/**
* @brief Tests full CONNECT→DATA→DISCONNECT cycle with high-frequency packed signals.
* Reassembles all fragments and verifies the total payload size.
*/
bool TestHighFrequency_DataIntegrity();
/**
* @brief Tests Initialise with MulticastGroup and explicit DataPort.
*/
bool TestInitialise_MulticastMode_Valid();
/**
* @brief Tests that DataPort defaults to Port+1 when MulticastGroup is set but DataPort is absent.
*/
bool TestInitialise_MulticastMode_DefaultDataPort();
/**
* @brief Tests that DataPort equal to Port is rejected.
*/
bool TestInitialise_MulticastMode_InvalidDataPort();
/**
* @brief Tests PrepareNextState in multicast mode: TCP listener opens, data socket connects.
*/
bool TestPrepareNextState_Multicast();
/**
* @brief Tests full TCP CONNECT → CONFIG → DATA via multicast → DISCONNECT on loopback.
*/
bool TestExecute_MulticastConnectDataDisconnect();
/**
* @brief Tests that Accumulate publishes every RT cycle it batches.
* @details Drives 600 cycles at a rate the sender thread trivially keeps up
* with, and sums the numSamples field of every DATA packet that arrives.
* A batch promoted to the ready buffer but never sent — because the wake-up
* announcing it was swallowed — shows up here as missing cycles, which a
* consumer sees as a hole in the time series.
*/
bool TestAccumulate_EveryPublishedCycleReachesTheWire();
};
#endif /* UDPSTREAMERTEST_H_ */