From 1c78afa5ef80d04b5f34cecac69d357a5040d881 Mon Sep 17 00:00:00 2001 From: Chris Izatt Date: Wed, 10 Jun 2026 11:17:10 +0100 Subject: [PATCH] =?UTF-8?q?lab:=20complete=20Step=206=20=E2=80=94=20Verify?= =?UTF-8?q?ingSlotProvider,=20IDiagSink,=20scenario=20pump?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the Milestone 1 payoff step per the lab README: - Ports/IDiagSink: the RT-safe sticky-counter seam (increment-only, no logging, no allocation); Lab/StickyCounterSink as its fixed-array relaxed-atomic implementation for tests and the future dext dump. - Lab/VerifyingSlotProvider: decorator over any IAmdtpTxSlotProvider asserting P1-P4 on every PublishSlot — cadence window (6000/8000) and N,D,D,D run shape, DBC continuity (no-data carries unchanged), CIP bit-exactness re-parsed from the published wire image (Q0/Q1 fields, 8-byte no-data, byteCount vs frames*dbs), and gapless frame tiling. Observer not gate: packets always forward; violations resync so one fault counts once. Structural constants are asserted, stream constants (SID, frames-per-data) are learned from the wire — the verifier shares no code with the packetizer it judges. - Step 6 seams (additive only, no behavior change): controller BindLabSlotProvider() to interpose Verifying(Fake) between engine and ring, and PayloadCounters()/PayloadWriterCounters() accessors to read the writer's miss buckets from scenarios. - Tests/VerifyingSlotProviderTests: instrument self-tests — a hand-rolled golden driver (independent of AmdtpTxPacketizer) plus one corruption case per violation kind, each proving the targeted counter fires exactly once with no cross-talk. - Tests/VerifierScenarioTests: the scenario pump (controller -> engine -> Verifying(Fake)) — regular 512-frame soak past 1e6 cycles with zero violations, irregular frame counts, skipped callback (silence in structurally valid packets, never corruption), sample-time jumps landing in framesWithoutPacket/framesOutsidePacket, and stream restart. Suite: 17440 checks, 0 failures (was 8341). Dext target still compiles. Milestone 1 exit criteria are now executable checks. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Core/VirtualAudioDeviceController.cpp | 14 + .../Core/VirtualAudioDeviceController.hpp | 10 + ADKVirtualAudioLab/Lab/StickyCounterSink.hpp | 51 +++ .../Lab/VerifyingSlotProvider.cpp | 301 +++++++++++++++ .../Lab/VerifyingSlotProvider.hpp | 188 ++++++++++ ADKVirtualAudioLab/Ports/IDiagSink.hpp | 21 ++ .../Audio/DICE/DiceTxStreamEngine.cpp | 5 + .../Audio/DICE/DiceTxStreamEngine.hpp | 3 + .../Tests/VerifierScenarioTests.cpp | 301 +++++++++++++++ .../Tests/VerifyingSlotProviderTests.cpp | 348 ++++++++++++++++++ ADKVirtualAudioLab/Tests/main.cpp | 4 + 11 files changed, 1246 insertions(+) create mode 100644 ADKVirtualAudioLab/Lab/StickyCounterSink.hpp create mode 100644 ADKVirtualAudioLab/Lab/VerifyingSlotProvider.cpp create mode 100644 ADKVirtualAudioLab/Lab/VerifyingSlotProvider.hpp create mode 100644 ADKVirtualAudioLab/Ports/IDiagSink.hpp create mode 100644 ADKVirtualAudioLab/Tests/VerifierScenarioTests.cpp create mode 100644 ADKVirtualAudioLab/Tests/VerifyingSlotProviderTests.cpp diff --git a/ADKVirtualAudioLab/Core/VirtualAudioDeviceController.cpp b/ADKVirtualAudioLab/Core/VirtualAudioDeviceController.cpp index 6dd74a9d..bdfc2150 100644 --- a/ADKVirtualAudioLab/Core/VirtualAudioDeviceController.cpp +++ b/ADKVirtualAudioLab/Core/VirtualAudioDeviceController.cpp @@ -76,6 +76,20 @@ void VirtualAudioDeviceController::SubmitWriteEnd( audioIOPath_.HandleWriteEnd(output); } +void VirtualAudioDeviceController::BindLabSlotProvider( + Protocols::Audio::AMDTP::IAmdtpTxSlotProvider* provider) noexcept { + txEngine_.BindSlotProvider( + provider != nullptr + ? provider + : static_cast( + &fakeSlotProvider_)); +} + +const Protocols::Audio::AMDTP::AmdtpPayloadWriterCounters& +VirtualAudioDeviceController::PayloadCounters() const noexcept { + return txEngine_.PayloadWriterCounters(); +} + const Lab::FakeIsochTxSlotProvider& VirtualAudioDeviceController::FakeSlotProvider() const noexcept { return fakeSlotProvider_; diff --git a/ADKVirtualAudioLab/Core/VirtualAudioDeviceController.hpp b/ADKVirtualAudioLab/Core/VirtualAudioDeviceController.hpp index f7de9821..76dd5001 100644 --- a/ADKVirtualAudioLab/Core/VirtualAudioDeviceController.hpp +++ b/ADKVirtualAudioLab/Core/VirtualAudioDeviceController.hpp @@ -31,6 +31,16 @@ class VirtualAudioDeviceController final { void SubmitWriteEnd(const Protocols::Audio::AMDTP::HostAudioBufferView& output) noexcept; + // Step 6 seam: interpose a decorator (Verifying(Fake)) between the engine + // and the fake ring. Call after Initialize(), which binds the bare fake; + // passing nullptr restores the fake. The caller owns the provider and + // typically wraps FakeSlotProvider() itself. + void BindLabSlotProvider( + Protocols::Audio::AMDTP::IAmdtpTxSlotProvider* provider) noexcept; + + const Protocols::Audio::AMDTP::AmdtpPayloadWriterCounters& + PayloadCounters() const noexcept; + const Lab::FakeIsochTxSlotProvider& FakeSlotProvider() const noexcept; Lab::FakeIsochTxSlotProvider& FakeSlotProvider() noexcept; diff --git a/ADKVirtualAudioLab/Lab/StickyCounterSink.hpp b/ADKVirtualAudioLab/Lab/StickyCounterSink.hpp new file mode 100644 index 00000000..c991cdd7 --- /dev/null +++ b/ADKVirtualAudioLab/Lab/StickyCounterSink.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "../Ports/IDiagSink.hpp" + +#include +#include + +namespace ASFW::Lab { + +// Fixed-size sticky counter store behind Ports::IDiagSink. Relaxed atomics +// so the same object can be incremented from a real-time IO callback and +// read from test code or a StopIO dump; counters only ever grow (sticky) +// until Reset(), which is a non-RT operation. +class StickyCounterSink final : public Ports::IDiagSink { +public: + static constexpr uint32_t kCapacity = 64; + + StickyCounterSink() noexcept = default; + + void Increment(uint32_t counterId, uint64_t delta) noexcept override { + if (counterId >= kCapacity) { + overflowedIds_.fetch_add(1, std::memory_order_relaxed); + return; + } + counters_[counterId].fetch_add(delta, std::memory_order_relaxed); + } + + [[nodiscard]] uint64_t Value(uint32_t counterId) const noexcept { + if (counterId >= kCapacity) { + return 0; + } + return counters_[counterId].load(std::memory_order_relaxed); + } + + [[nodiscard]] uint64_t OverflowedIds() const noexcept { + return overflowedIds_.load(std::memory_order_relaxed); + } + + void Reset() noexcept { + for (auto& counter : counters_) { + counter.store(0, std::memory_order_relaxed); + } + overflowedIds_.store(0, std::memory_order_relaxed); + } + +private: + std::atomic counters_[kCapacity]{}; + std::atomic overflowedIds_{0}; +}; + +} // namespace ASFW::Lab diff --git a/ADKVirtualAudioLab/Lab/VerifyingSlotProvider.cpp b/ADKVirtualAudioLab/Lab/VerifyingSlotProvider.cpp new file mode 100644 index 00000000..77b10751 --- /dev/null +++ b/ADKVirtualAudioLab/Lab/VerifyingSlotProvider.cpp @@ -0,0 +1,301 @@ +#include "VerifyingSlotProvider.hpp" + +namespace ASFW::Lab { + +// Wire-facts checked here (CIP layout per IEC 61883-1, cross-checked against +// CipHeader.cpp and the Linux/FFADO golden rules recorded in the README): +// +// Q0: [31:30]=00 [29:24]=SID [23:16]=DBS [15:8]=FN/QPC/SPH/rsv (all 0 +// for AM824) [7:0]=DBC +// Q1: [31:30]=10 (EOH) [29:24]=FMT=0x10 [23:16]=FDF [15:0]=SYT +// +// Data packet: byteCount == 8 + framesInPacket * dbs * 4, FDF == 0x02 +// (48 kHz), SYT field == packet.syt verbatim (0xFFFF is +// legal while the TX clock is invalid — Milestone 1). +// No-data packet: CIP-header-only (byteCount == 8), FDF == 0xFF, +// SYT == 0xFFFF, DBC carried unchanged, zero frames. + +namespace { + +using Protocols::Audio::AMDTP::PreparedTxPacket; +using Protocols::Audio::AMDTP::TxPacketSlotView; + +constexpr uint32_t kCipHeaderBytes = 8; +constexpr uint32_t kBytesPerSlot = 4; +constexpr uint8_t kFdfNoData = 0xFF; +constexpr uint16_t kSytNoInfo = 0xFFFF; +constexpr uint8_t kFmtAm824 = 0x10; + +inline uint32_t ReadBE32(const uint8_t* src) noexcept { + return (static_cast(src[0]) << 24) | + (static_cast(src[1]) << 16) | + (static_cast(src[2]) << 8) | static_cast(src[3]); +} + +} // namespace + +VerifyingSlotProvider::VerifyingSlotProvider( + Protocols::Audio::AMDTP::IAmdtpTxSlotProvider& inner) noexcept + : inner_(&inner) {} + +VerifyingSlotProvider::VerifyingSlotProvider( + Protocols::Audio::AMDTP::IAmdtpTxSlotProvider& inner, + const Config& config) noexcept + : inner_(&inner), config_(config) {} + +void VerifyingSlotProvider::Configure(const Config& config) noexcept { + config_ = config; + Reset(); +} + +void VerifyingSlotProvider::Reset() noexcept { + for (auto& tracked : trackedSlots_) { + tracked = TrackedSlot{}; + } + + packetIndexValid_ = false; + lastPacketIndex_ = 0; + dbcValid_ = false; + expectedDbc_ = 0; + nextFrameValid_ = false; + expectedNextFrame_ = 0; + sidValid_ = false; + learnedSid_ = 0; + framesPerDataValid_ = false; + learnedFramesPerData_ = 0; + consecutiveData_ = 0; + consecutiveNoData_ = 0; + windowPackets_ = 0; + windowDataPackets_ = 0; + + for (auto& counter : counters_) { + counter.store(0, std::memory_order_relaxed); + } + firstViolationValid_.store(false, std::memory_order_relaxed); + firstViolationId_.store(0, std::memory_order_relaxed); + firstViolationPacketIndex_.store(0, std::memory_order_relaxed); +} + +bool VerifyingSlotProvider::AcquireWritableSlot(uint32_t packetIndex, + TxPacketSlotView& outSlot) noexcept { + Count(VerifierCounterId::kAcquireCalls); + + if (inner_ == nullptr || !inner_->AcquireWritableSlot(packetIndex, outSlot)) { + Count(VerifierCounterId::kAcquireFailures); + return false; + } + + TrackedSlot& tracked = trackedSlots_[packetIndex % kTrackedSlots]; + tracked.packetIndex = packetIndex; + tracked.bytes = outSlot.bytes; + tracked.capacityBytes = outSlot.capacityBytes; + tracked.valid = true; + tracked.published = false; + return true; +} + +void VerifyingSlotProvider::PublishSlot(const PreparedTxPacket& packet) noexcept { + Count(VerifierCounterId::kPacketsPublished); + Count(packet.isData ? VerifierCounterId::kDataPackets + : VerifierCounterId::kNoDataPackets); + + // Packet-index contiguity (resync on violation so one gap counts once). + if (packetIndexValid_ && packet.packetIndex != lastPacketIndex_ + 1) { + Violation(VerifierCounterId::kP1PacketIndexGapViolation, + packet.packetIndex); + } + lastPacketIndex_ = packet.packetIndex; + packetIndexValid_ = true; + + // Wire-image lookup from the acquire ring. + TrackedSlot& tracked = trackedSlots_[packet.packetIndex % kTrackedSlots]; + const bool trackedMatches = tracked.valid && + tracked.packetIndex == packet.packetIndex && + tracked.bytes != nullptr && !tracked.published; + if (!trackedMatches) { + Violation(VerifierCounterId::kP3UnacquiredPublishViolation, + packet.packetIndex); + } else { + tracked.published = true; + } + + CheckStructure(packet, trackedMatches ? &tracked : nullptr); + CheckDbcContinuity(packet); + CheckFrameTiling(packet); + if (config_.blockingMode) { + CheckCadence(packet); + } + + if (inner_ != nullptr) { + inner_->PublishSlot(packet); + } +} + +uint32_t VerifyingSlotProvider::SlotCount() const noexcept { + return (inner_ != nullptr) ? inner_->SlotCount() : 0; +} + +VerifierSnapshot VerifyingSlotProvider::Snapshot() const noexcept { + VerifierSnapshot snapshot{}; + for (uint32_t id = 0; id < static_cast(VerifierCounterId::kIdLimit); + ++id) { + snapshot.counters[id] = counters_[id].load(std::memory_order_relaxed); + } + snapshot.firstViolationValid = + firstViolationValid_.load(std::memory_order_relaxed); + snapshot.firstViolationId = firstViolationId_.load(std::memory_order_relaxed); + snapshot.firstViolationPacketIndex = + firstViolationPacketIndex_.load(std::memory_order_relaxed); + return snapshot; +} + +void VerifyingSlotProvider::Count(VerifierCounterId id, uint64_t delta) noexcept { + counters_[static_cast(id)].fetch_add(delta, + std::memory_order_relaxed); + if (config_.diagSink != nullptr) { + config_.diagSink->Increment(static_cast(id), delta); + } +} + +void VerifyingSlotProvider::Violation(VerifierCounterId id, + uint64_t packetIndex) noexcept { + Count(id); + if (!firstViolationValid_.load(std::memory_order_relaxed)) { + firstViolationId_.store(static_cast(id), + std::memory_order_relaxed); + firstViolationPacketIndex_.store(packetIndex, std::memory_order_relaxed); + firstViolationValid_.store(true, std::memory_order_relaxed); + } +} + +void VerifyingSlotProvider::CheckStructure(const PreparedTxPacket& packet, + const TrackedSlot* tracked) noexcept { + // Byte-count vs packet kind (and vs the acquired capacity when known). + const uint32_t expectedBytes = + packet.isData ? (kCipHeaderBytes + + packet.framesInPacket * packet.dbs * kBytesPerSlot) + : kCipHeaderBytes; + bool byteCountOk = (packet.byteCount == expectedBytes); + if (tracked != nullptr && packet.byteCount > tracked->capacityBytes) { + byteCountOk = false; + } + if (!byteCountOk) { + Violation(VerifierCounterId::kP3ByteCountViolation, packet.packetIndex); + } + + if (tracked == nullptr || tracked->capacityBytes < kCipHeaderBytes) { + return; // no wire image to parse; the unacquired violation already fired + } + + const uint32_t q0 = ReadBE32(tracked->bytes); + const uint32_t q1 = ReadBE32(tracked->bytes + 4); + + // Q0: [31:30]=00, SID stable, DBS == metadata, FN/QPC/SPH/rsv all zero, + // DBC == metadata. + const uint8_t q0Sid = static_cast((q0 >> 24) & 0x3F); + bool q0Ok = ((q0 >> 30) == 0u) && + (((q0 >> 16) & 0xFFu) == packet.dbs) && + (((q0 >> 8) & 0xFFu) == 0u) && + ((q0 & 0xFFu) == packet.dbc); + if (q0Ok) { + if (!sidValid_) { + learnedSid_ = q0Sid; + sidValid_ = true; + } else if (q0Sid != learnedSid_) { + q0Ok = false; + } + } + if (!q0Ok) { + Violation(VerifierCounterId::kP3CipQ0Violation, packet.packetIndex); + } + + // Q1: EOH '10', FMT 0x10, FDF per kind, SYT field == metadata (and the + // no-data rules: FDF 0xFF + SYT 0xFFFF). + const uint8_t q1Fdf = static_cast((q1 >> 16) & 0xFF); + const uint16_t q1Syt = static_cast(q1 & 0xFFFF); + bool q1Ok = ((q1 >> 30) == 0b10u) && + (((q1 >> 24) & 0x3Fu) == kFmtAm824) && + (q1Syt == packet.syt); + if (packet.isData) { + q1Ok = q1Ok && (q1Fdf == config_.expectedDataFdf); + } else { + q1Ok = q1Ok && (q1Fdf == kFdfNoData) && (q1Syt == kSytNoInfo); + } + if (!q1Ok) { + Violation(VerifierCounterId::kP3CipQ1Violation, packet.packetIndex); + } +} + +void VerifyingSlotProvider::CheckDbcContinuity( + const PreparedTxPacket& packet) noexcept { + // Golden rule: every packet carries the running DBC; only data packets + // advance it (by their data-block count == framesInPacket for AM824). + if (dbcValid_ && packet.dbc != expectedDbc_) { + Violation(VerifierCounterId::kP2DbcViolation, packet.packetIndex); + } + expectedDbc_ = packet.isData + ? static_cast(packet.dbc + packet.framesInPacket) + : packet.dbc; + dbcValid_ = true; +} + +void VerifyingSlotProvider::CheckFrameTiling( + const PreparedTxPacket& packet) noexcept { + if (packet.isData) { + if (packet.framesInPacket == 0) { + Violation(VerifierCounterId::kP4FrameCountViolation, + packet.packetIndex); + } else if (!framesPerDataValid_) { + learnedFramesPerData_ = packet.framesInPacket; + framesPerDataValid_ = true; + } else if (packet.framesInPacket != learnedFramesPerData_) { + Violation(VerifierCounterId::kP4FrameCountViolation, + packet.packetIndex); + } + } else if (packet.framesInPacket != 0) { + Violation(VerifierCounterId::kP4FrameCountViolation, packet.packetIndex); + } + + // Both kinds must sit at the gapless frame cursor; only data advances it. + if (nextFrameValid_ && packet.firstAudioFrame != expectedNextFrame_) { + Violation(VerifierCounterId::kP4FrameTilingViolation, packet.packetIndex); + } + expectedNextFrame_ = packet.firstAudioFrame + packet.framesInPacket; + nextFrameValid_ = true; +} + +void VerifyingSlotProvider::CheckCadence(const PreparedTxPacket& packet) noexcept { + // Run-length shape of the blocking 48 kHz N,D,D,D pattern: data runs of + // at most 3, isolated no-data packets. + if (packet.isData) { + ++consecutiveData_; + consecutiveNoData_ = 0; + if (consecutiveData_ > kMaxConsecutiveData) { + Violation(VerifierCounterId::kP1CadenceRunViolation, + packet.packetIndex); + } + } else { + ++consecutiveNoData_; + consecutiveData_ = 0; + if (consecutiveNoData_ > kMaxConsecutiveNoData) { + Violation(VerifierCounterId::kP1CadenceRunViolation, + packet.packetIndex); + } + } + + // Tumbling 8000-packet window: exactly 6000 data packets per window. + ++windowPackets_; + if (packet.isData) { + ++windowDataPackets_; + } + if (windowPackets_ == kCadenceWindowPackets) { + if (windowDataPackets_ != kCadenceWindowDataPackets) { + Violation(VerifierCounterId::kP1CadenceWindowViolation, + packet.packetIndex); + } + windowPackets_ = 0; + windowDataPackets_ = 0; + } +} + +} // namespace ASFW::Lab diff --git a/ADKVirtualAudioLab/Lab/VerifyingSlotProvider.hpp b/ADKVirtualAudioLab/Lab/VerifyingSlotProvider.hpp new file mode 100644 index 00000000..6105764b --- /dev/null +++ b/ADKVirtualAudioLab/Lab/VerifyingSlotProvider.hpp @@ -0,0 +1,188 @@ +#pragma once + +#include "../Ports/IAmdtpTxSlotProvider.hpp" +#include "../Ports/IDiagSink.hpp" + +#include +#include + +namespace ASFW::Lab { + +// Counter ids reported by VerifyingSlotProvider (totals + one sticky counter +// per invariant violation kind). Ids are sparse on purpose: the tens digit +// groups them by the README invariant they belong to (P1/P2/P3/P4), leaving +// room to add kinds without renumbering. They double as Ports::IDiagSink ids. +enum class VerifierCounterId : uint32_t { + // Totals + kPacketsPublished = 0, + kDataPackets = 1, + kNoDataPackets = 2, + kAcquireCalls = 3, + kAcquireFailures = 4, + + // P1 — cadence / stream shape + kP1CadenceWindowViolation = 10, // tumbling 8000-packet window != 6000 data + kP1CadenceRunViolation = 11, // >3 consecutive data or >1 consecutive no-data + kP1PacketIndexGapViolation = 12, // packetIndex != previous + 1 + + // P2 — DBC continuity + kP2DbcViolation = 20, // dbc != running expectation (no-data must carry unchanged) + + // P3 — CIP bit-exactness / packet structure + kP3ByteCountViolation = 30, // byteCount vs kind/frames/dbs (or > capacity) + kP3CipQ0Violation = 31, // sid/dbs/fn/qpc/sph/reserved/dbc fields + kP3CipQ1Violation = 32, // eoh/fmt/fdf/syt fields + kP3UnacquiredPublishViolation = 33, // publish without a matching live acquire + + // P4 — frame tiling + kP4FrameTilingViolation = 40, // firstAudioFrame != expected next frame + kP4FrameCountViolation = 41, // data frame count inconsistent / no-data frames != 0 + + kIdLimit = 42, +}; + +struct VerifierSnapshot final { + uint64_t counters[static_cast(VerifierCounterId::kIdLimit)]{}; + + bool firstViolationValid{false}; + uint32_t firstViolationId{0}; + uint64_t firstViolationPacketIndex{0}; + + [[nodiscard]] uint64_t Value(VerifierCounterId id) const noexcept { + return counters[static_cast(id)]; + } + + [[nodiscard]] uint64_t TotalViolations() const noexcept { + uint64_t sum = 0; + for (uint32_t id = 10; + id < static_cast(VerifierCounterId::kIdLimit); ++id) { + sum += counters[id]; + } + return sum; + } +}; + +// The Step 6 instrument (see README, "Key design decisions"): a decorator +// wrapping ANY IAmdtpTxSlotProvider that asserts the P1–P4 invariants on +// every PublishSlot and records violations as sticky counters — never +// logging, never blocking, never altering the wrapped provider's behavior. +// The same object is meant to run in three contexts: host tests +// (Verifying(Fake)), the lab dext under real HAL pacing, and later ASFW +// bring-up (Verifying(RealDmaRing)). +// +// Design decisions: +// +// 1. Observer, not gate: packets are forwarded to the inner provider even +// when they violate an invariant. The lab needs the downstream effects of +// a bad packet to stay observable; rejecting would also make the +// decorator change behavior, which it must never do. +// 2. Resync-after-violation: every continuity check (index, DBC, frame +// tiling) re-anchors its expectation on the observed value after a +// violation, so one corruption increments exactly one counter instead of +// cascading — that keeps counter signatures readable as diagnostics. +// 3. Structural constants are checked, stream constants are learned: FMT, +// the 48 kHz data FDF, the no-data rules, and the metadata↔bytes +// agreement are asserted outright; SID and frames-per-data-packet are +// learned from the first packet and then held constant. The verifier +// deliberately re-derives nothing from the packetizer's internals — it +// checks observable wire facts, not a second copy of the implementation. +// 4. Acquire tracking mirrors publish-at-prepare: AcquireWritableSlot +// records (index, bytes, capacity) in a small ring keyed by +// packetIndex % kTrackedSlots; PublishSlot consumes the entry to parse +// the wire image. kTrackedSlots mirrors the IT ring geometry (256). +// 5. All counters are relaxed atomics and every increment is mirrored to an +// optional Ports::IDiagSink — RT-safe by construction. +class VerifyingSlotProvider final + : public Protocols::Audio::AMDTP::IAmdtpTxSlotProvider { +public: + static constexpr uint32_t kTrackedSlots = 256; + static constexpr uint32_t kCadenceWindowPackets = 8000; + static constexpr uint32_t kCadenceWindowDataPackets = 6000; + static constexpr uint32_t kMaxConsecutiveData = 3; + static constexpr uint32_t kMaxConsecutiveNoData = 1; + + struct Config final { + bool blockingMode{true}; // enables P1 run-length + 8000-window checks + uint8_t expectedDataFdf{0x02}; // AM824 | 48 kHz (the lab is 48 k-only) + Ports::IDiagSink* diagSink{nullptr}; + }; + + explicit VerifyingSlotProvider( + Protocols::Audio::AMDTP::IAmdtpTxSlotProvider& inner) noexcept; + + VerifyingSlotProvider(Protocols::Audio::AMDTP::IAmdtpTxSlotProvider& inner, + const Config& config) noexcept; + + void Configure(const Config& config) noexcept; + + // Clears counters and all learned/continuity state. Call alongside the + // engine's ResetForStart when a scenario restarts the stream. + void Reset() noexcept; + + bool AcquireWritableSlot( + uint32_t packetIndex, + Protocols::Audio::AMDTP::TxPacketSlotView& outSlot) noexcept override; + + void PublishSlot( + const Protocols::Audio::AMDTP::PreparedTxPacket& packet) noexcept override; + + uint32_t SlotCount() const noexcept override; + + [[nodiscard]] VerifierSnapshot Snapshot() const noexcept; + +private: + struct TrackedSlot final { + uint64_t packetIndex{0}; + const uint8_t* bytes{nullptr}; + uint32_t capacityBytes{0}; + bool valid{false}; + bool published{false}; + }; + + void Count(VerifierCounterId id, uint64_t delta = 1) noexcept; + void Violation(VerifierCounterId id, uint64_t packetIndex) noexcept; + + void CheckStructure(const Protocols::Audio::AMDTP::PreparedTxPacket& packet, + const TrackedSlot* tracked) noexcept; + void CheckDbcContinuity( + const Protocols::Audio::AMDTP::PreparedTxPacket& packet) noexcept; + void CheckFrameTiling( + const Protocols::Audio::AMDTP::PreparedTxPacket& packet) noexcept; + void CheckCadence( + const Protocols::Audio::AMDTP::PreparedTxPacket& packet) noexcept; + + Protocols::Audio::AMDTP::IAmdtpTxSlotProvider* inner_{nullptr}; + Config config_{}; + + TrackedSlot trackedSlots_[kTrackedSlots]{}; + + // Continuity state (single writer: the IO/pump thread). + bool packetIndexValid_{false}; + uint32_t lastPacketIndex_{0}; + + bool dbcValid_{false}; + uint8_t expectedDbc_{0}; + + bool nextFrameValid_{false}; + uint64_t expectedNextFrame_{0}; + + bool sidValid_{false}; + uint8_t learnedSid_{0}; + + bool framesPerDataValid_{false}; + uint32_t learnedFramesPerData_{0}; + + uint32_t consecutiveData_{0}; + uint32_t consecutiveNoData_{0}; + + uint32_t windowPackets_{0}; + uint32_t windowDataPackets_{0}; + + std::atomic + counters_[static_cast(VerifierCounterId::kIdLimit)]{}; + std::atomic firstViolationValid_{false}; + std::atomic firstViolationId_{0}; + std::atomic firstViolationPacketIndex_{0}; +}; + +} // namespace ASFW::Lab diff --git a/ADKVirtualAudioLab/Ports/IDiagSink.hpp b/ADKVirtualAudioLab/Ports/IDiagSink.hpp new file mode 100644 index 00000000..6e1680b5 --- /dev/null +++ b/ADKVirtualAudioLab/Ports/IDiagSink.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace ASFW::Ports { + +// RT-safe diagnostics sink (see README, Architecture / Ports). +// +// Instruments report through this seam instead of logging: counter ids are +// small stable integers owned by the reporting instrument, increments must be +// safe on a real-time thread (no allocation, no locks, no IO). Production +// binds this to whatever diagnostics plumbing exists there; the lab binds it +// to Lab::StickyCounterSink and dumps at StopIO or from test code. +class IDiagSink { +public: + virtual ~IDiagSink() = default; + + virtual void Increment(uint32_t counterId, uint64_t delta) noexcept = 0; +}; + +} // namespace ASFW::Ports diff --git a/ADKVirtualAudioLab/Protocols/Audio/DICE/DiceTxStreamEngine.cpp b/ADKVirtualAudioLab/Protocols/Audio/DICE/DiceTxStreamEngine.cpp index 4389584d..b445162a 100644 --- a/ADKVirtualAudioLab/Protocols/Audio/DICE/DiceTxStreamEngine.cpp +++ b/ADKVirtualAudioLab/Protocols/Audio/DICE/DiceTxStreamEngine.cpp @@ -104,6 +104,11 @@ const DiceTxEngineCounters& DiceTxStreamEngine::Counters() const noexcept { return counters_; } +const AMDTP::AmdtpPayloadWriterCounters& +DiceTxStreamEngine::PayloadWriterCounters() const noexcept { + return payloadWriter_.Counters(); +} + AMDTP::AmdtpTxPolicy DiceTxStreamEngine::BuildTxPolicy( const DiceDeviceQuirks& quirks) const noexcept { AMDTP::AmdtpTxPolicy policy{}; diff --git a/ADKVirtualAudioLab/Protocols/Audio/DICE/DiceTxStreamEngine.hpp b/ADKVirtualAudioLab/Protocols/Audio/DICE/DiceTxStreamEngine.hpp index 454e2ab3..8f1977f0 100644 --- a/ADKVirtualAudioLab/Protocols/Audio/DICE/DiceTxStreamEngine.hpp +++ b/ADKVirtualAudioLab/Protocols/Audio/DICE/DiceTxStreamEngine.hpp @@ -40,6 +40,9 @@ class DiceTxStreamEngine final { [[nodiscard]] const DiceTxEngineCounters& Counters() const noexcept; + [[nodiscard]] const AMDTP::AmdtpPayloadWriterCounters& + PayloadWriterCounters() const noexcept; + private: AMDTP::AmdtpTxPolicy BuildTxPolicy(const DiceDeviceQuirks& quirks) const noexcept; diff --git a/ADKVirtualAudioLab/Tests/VerifierScenarioTests.cpp b/ADKVirtualAudioLab/Tests/VerifierScenarioTests.cpp new file mode 100644 index 00000000..811c32ce --- /dev/null +++ b/ADKVirtualAudioLab/Tests/VerifierScenarioTests.cpp @@ -0,0 +1,301 @@ +#include "TestHarness.hpp" + +#include "../Core/VirtualAudioDeviceController.hpp" +#include "../Lab/VerifyingSlotProvider.hpp" +#include "../Protocols/Audio/DICE/DiceTxStreamEngine.hpp" + +// Step 6 scenario pump (README, Milestone 1 exit criteria): WriteEnd-shaped +// schedules driving controller → engine → Verifying(Fake). The regular +// schedule must run >= 1e6 cycles with zero invariant violations; the +// adversarial schedules (irregular frame counts, skipped callback, +// sample-time jumps) must produce their *expected* counter signatures — a +// missed window shows up in the payload-writer miss buckets, never as a +// structural violation or silent corruption. + +namespace ASFW::LabTests { + +using Lab::VerifierCounterId; +using Lab::VerifierSnapshot; +using Lab::VerifyingSlotProvider; +using Protocols::Audio::AMDTP::HostAudioBufferView; +using Protocols::Audio::DICE::DiceDeviceIdentity; + +namespace { + +constexpr uint32_t kChannels = 2; +constexpr uint32_t kRingFrames = 4096; + +// Matches DiceTxEngineTests: resolves to the Focusrite profile +// (Raw24-in-32 BE host->device encoding). +constexpr DiceDeviceIdentity kFocusriteIdentity{0x130E01020304ULL, 0x00130E, 1}; + +// The pump: owns the controller, the Verifying(Fake) interposition, and the +// host-side PCM ring (mirroring the WriteEnd producer: the view's pointer +// sits at ring offset firstFrame % frameCapacity; the payload writer owns +// the wrap arithmetic). +struct LabPump final { + Driver::VirtualAudioDeviceController controller{}; + VerifyingSlotProvider verifier; + float ring[kRingFrames * kChannels]{}; + uint32_t nextPacketIndex{0}; + uint64_t exposedFrames{0}; + uint64_t prepareFailures{0}; + + LabPump() noexcept : verifier(controller.FakeSlotProvider()) {} + + bool Init() noexcept { + if (!controller.Initialize() || + !controller.SelectProfile(kFocusriteIdentity) || + !controller.ConfigureOutputStream(48000, kChannels, kRingFrames)) { + return false; + } + controller.BindLabSlotProvider(&verifier); + controller.ResetTransportLab(0, 0); + verifier.Reset(); + return true; + } + + // Prepare packets until the exposed frame timeline covers endFrame. + void PrepareCoverage(uint64_t endFrame) noexcept { + while (exposedFrames < endFrame) { + if (!controller.PrepareLabPacket(nextPacketIndex, 0xFFFF, false)) { + ++prepareFailures; + return; + } + const auto* published = + controller.FakeSlotProvider().PublishedPacket(nextPacketIndex); + if (published != nullptr && published->isData) { + exposedFrames += published->framesInPacket; + } + ++nextPacketIndex; + } + } + + void FillConstant(uint64_t firstFrame, uint32_t frameCount, + float value) noexcept { + for (uint32_t i = 0; i < frameCount; ++i) { + const uint32_t pos = + static_cast((firstFrame + i) % kRingFrames); + for (uint32_t ch = 0; ch < kChannels; ++ch) { + ring[pos * kChannels + ch] = value; + } + } + } + + HostAudioBufferView View(uint64_t firstFrame, uint32_t frameCount) noexcept { + HostAudioBufferView view{}; + view.interleavedFloat32 = + ring + (firstFrame % kRingFrames) * kChannels; + view.firstFrame = firstFrame; + view.frameCount = frameCount; + view.frameCapacity = kRingFrames; + view.channels = kChannels; + return view; + } + + // One well-behaved WriteEnd callback: cover, fill, submit. + void Callback(uint64_t sampleTime, uint32_t frames, float value) noexcept { + PrepareCoverage(sampleTime + frames); + FillConstant(sampleTime, frames, value); + controller.SubmitWriteEnd(View(sampleTime, frames)); + } +}; + +void CheckAllGreen(TestContext& ctx, const LabPump& pump) { + const VerifierSnapshot snapshot = pump.verifier.Snapshot(); + CHECK_EQ_U64(ctx, snapshot.TotalViolations(), 0); + CHECK(ctx, !snapshot.firstViolationValid); + CHECK_EQ_U64(ctx, pump.prepareFailures, 0); +} + +} // namespace + +void RunVerifierScenarioTests(TestContext& ctx) { + // Scenario A — regular schedule soak: >= 1e6 cycles, zero violations. + { + LabPump pump{}; + CHECK(ctx, pump.Init()); + + uint64_t sampleTime = 0; + while (pump.verifier.Snapshot().Value( + VerifierCounterId::kPacketsPublished) < 1000000) { + pump.Callback(sampleTime, 512, 0.25f); + sampleTime += 512; + } + + CheckAllGreen(ctx, pump); + const VerifierSnapshot snapshot = pump.verifier.Snapshot(); + const uint64_t total = + snapshot.Value(VerifierCounterId::kPacketsPublished); + const uint64_t data = snapshot.Value(VerifierCounterId::kDataPackets); + CHECK(ctx, total >= 1000000); + // Blocking 48k: 3 data per 4 packets. The tumbling-window check + // enforces it per window; for the whole run a trailing partial + // N,D,D,D group leaves 4*data - 3*total in [-3, 0]. + CHECK(ctx, data * 4 <= total * 3 && total * 3 - data * 4 <= 3); + + const auto& payload = pump.controller.PayloadCounters(); + CHECK_EQ_U64(ctx, payload.framesVisited.load(), + payload.framesWritten.load()); + CHECK_EQ_U64(ctx, payload.framesWithoutPacket.load(), 0); + CHECK_EQ_U64(ctx, payload.framesOutsidePacket.load(), 0); + } + + // Scenario B — irregular frame counts, contiguous sample time. + { + LabPump pump{}; + CHECK(ctx, pump.Init()); + + constexpr uint32_t kSizes[] = {512, 480, 53, 8, 511, 997, 64, 256}; + uint64_t sampleTime = 0; + for (int i = 0; i < 4000; ++i) { + const uint32_t frames = kSizes[i % 8]; + pump.Callback(sampleTime, frames, 0.25f); + sampleTime += frames; + } + + CheckAllGreen(ctx, pump); + const auto& payload = pump.controller.PayloadCounters(); + CHECK_EQ_U64(ctx, payload.framesVisited.load(), + payload.framesWritten.load()); + CHECK_EQ_U64(ctx, payload.framesWithoutPacket.load(), 0); + CHECK_EQ_U64(ctx, payload.framesOutsidePacket.load(), 0); + } + + // Scenario C — skipped callback: the gap frames are never written and + // must surface as silence in structurally valid packets, not as a + // violation. (Skip late in the run so the gap packets are still live in + // the 256-slot fake ring for payload inspection.) + { + LabPump pump{}; + CHECK(ctx, pump.Init()); + + uint64_t sampleTime = 0; + for (int i = 0; i < 22; ++i) { + pump.Callback(sampleTime, 512, 0.5f); + sampleTime += 512; + } + + const uint64_t gapStart = sampleTime; + sampleTime += 512; // the HAL skipped this callback entirely + const uint64_t gapEnd = sampleTime; + + for (int i = 0; i < 2; ++i) { + pump.Callback(sampleTime, 512, 0.5f); + sampleTime += 512; + } + + CheckAllGreen(ctx, pump); + const auto& payload = pump.controller.PayloadCounters(); + CHECK_EQ_U64(ctx, payload.framesVisited.load(), + payload.framesWritten.load()); + CHECK_EQ_U64(ctx, payload.framesVisited.load(), sampleTime - 512); + + // Find a data packet inside the gap and one in the written region. + bool sawSilentGapPacket = false; + bool sawWrittenPacket = false; + const uint32_t scanStart = + (pump.nextPacketIndex > 256) ? pump.nextPacketIndex - 256 : 0; + for (uint32_t index = scanStart; index < pump.nextPacketIndex; ++index) { + const auto* packet = + pump.controller.FakeSlotProvider().PublishedPacket(index); + if (packet == nullptr || packet->packetIndex != index || + !packet->isData) { + continue; + } + const uint8_t* payloadBytes = + pump.controller.FakeSlotProvider().SlotBytes(index) + 8; + const uint32_t payloadSize = + packet->framesInPacket * packet->dbs * 4; + + bool allZero = true; + for (uint32_t i = 0; i < payloadSize; ++i) { + if (payloadBytes[i] != 0) { + allZero = false; + break; + } + } + + if (packet->firstAudioFrame >= gapStart && + packet->firstAudioFrame + packet->framesInPacket <= gapEnd) { + sawSilentGapPacket = sawSilentGapPacket || allZero; + CHECK(ctx, allZero); + } else if (packet->firstAudioFrame >= gapEnd || + packet->firstAudioFrame + packet->framesInPacket <= + gapStart) { + // Written region (the 256-slot ring may retain only the + // post-gap side; pre-gap packets are typically evicted). + sawWrittenPacket = sawWrittenPacket || !allZero; + } + } + CHECK(ctx, sawSilentGapPacket); + CHECK(ctx, sawWrittenPacket); + } + + // Scenario D/E — sample-time jumps: ahead of the exposed timeline lands + // in framesWithoutPacket; behind the retired/evicted window lands in + // framesOutsidePacket. Structure stays green throughout. + { + LabPump pump{}; + CHECK(ctx, pump.Init()); + + uint64_t sampleTime = 0; + for (int i = 0; i < 8; ++i) { // warmup wraps the 256-slot ring + pump.Callback(sampleTime, 512, 0.25f); + sampleTime += 512; + } + + const auto& payload = pump.controller.PayloadCounters(); + const uint64_t writtenBefore = payload.framesWritten.load(); + + // D: a window far ahead of anything exposed (no PrepareCoverage). + const uint64_t jumpedTime = sampleTime + 1000000; + pump.FillConstant(jumpedTime, 512, 0.25f); + pump.controller.SubmitWriteEnd(pump.View(jumpedTime, 512)); + CHECK_EQ_U64(ctx, payload.framesWithoutPacket.load(), 512); + CHECK_EQ_U64(ctx, payload.framesWritten.load(), writtenBefore); + + // E: a window behind the live ring (frame 0 is long evicted). + pump.FillConstant(0, 512, 0.25f); + pump.controller.SubmitWriteEnd(pump.View(0, 512)); + CHECK_EQ_U64(ctx, payload.framesOutsidePacket.load(), 512); + CHECK_EQ_U64(ctx, payload.framesWritten.load(), writtenBefore); + + CheckAllGreen(ctx, pump); + + // Normal pacing resumes cleanly after both faults. + for (int i = 0; i < 4; ++i) { + pump.Callback(sampleTime, 512, 0.25f); + sampleTime += 512; + } + CheckAllGreen(ctx, pump); + CHECK_EQ_U64(ctx, payload.framesWithoutPacket.load(), 512); + CHECK_EQ_U64(ctx, payload.framesOutsidePacket.load(), 512); + } + + // Scenario F — stream restart: engine reset + verifier reset, then green. + { + LabPump pump{}; + CHECK(ctx, pump.Init()); + + uint64_t sampleTime = 0; + for (int i = 0; i < 8; ++i) { + pump.Callback(sampleTime, 512, 0.25f); + sampleTime += 512; + } + CheckAllGreen(ctx, pump); + + pump.controller.ResetTransportLab(0, 0); + pump.verifier.Reset(); + pump.exposedFrames = 0; + + sampleTime = 0; + for (int i = 0; i < 8; ++i) { + pump.Callback(sampleTime, 512, 0.25f); + sampleTime += 512; + } + CheckAllGreen(ctx, pump); + } +} + +} // namespace ASFW::LabTests diff --git a/ADKVirtualAudioLab/Tests/VerifyingSlotProviderTests.cpp b/ADKVirtualAudioLab/Tests/VerifyingSlotProviderTests.cpp new file mode 100644 index 00000000..c88240d4 --- /dev/null +++ b/ADKVirtualAudioLab/Tests/VerifyingSlotProviderTests.cpp @@ -0,0 +1,348 @@ +#include "TestHarness.hpp" + +#include "../Lab/FakeIsochTxSlotProvider.hpp" +#include "../Lab/StickyCounterSink.hpp" +#include "../Lab/VerifyingSlotProvider.hpp" +#include "../Protocols/Audio/IEC61883/CipHeader.hpp" + +#include + +// Self-tests for the Step 6 instrument: a verifier that cannot catch +// violations is worse than no verifier, so every violation kind gets a +// corruption case proving it fires — and, thanks to resync-after-violation, +// fires exactly once per injected fault, with no cross-talk into the other +// counters. Sequences are produced by a tiny hand-rolled golden driver +// (deliberately independent of AmdtpTxPacketizer: the instrument must not be +// validated against the code it will later judge). + +namespace ASFW::LabTests { + +using Lab::FakeIsochTxSlotProvider; +using Lab::StickyCounterSink; +using Lab::VerifierCounterId; +using Lab::VerifierSnapshot; +using Lab::VerifyingSlotProvider; +using Protocols::Audio::AMDTP::PreparedTxPacket; +using Protocols::Audio::AMDTP::TxPacketSlotView; +using Protocols::Audio::IEC61883::CipHeaderBuilder; +using Protocols::Audio::IEC61883::CipHeaderConfig; + +namespace { + +constexpr uint8_t kDbs = 2; +constexpr uint8_t kFramesPerData = 8; + +inline void WriteBE32(uint8_t* dest, uint32_t value) noexcept { + dest[0] = static_cast(value >> 24); + dest[1] = static_cast(value >> 16); + dest[2] = static_cast(value >> 8); + dest[3] = static_cast(value); +} + +// Mutator hook applied after the golden packet is built, before publish. +using Mutator = std::function; + +// Hand-rolled producer of golden AMDTP blocking-48k sequences. EmitAuto() +// follows the N,D,D,D cadence (6 frames pending per cycle, emit 8 when >= 8); +// EmitExplicit() lets a test script an arbitrary data/no-data shape while the +// driver keeps DBC and frame tiling consistent. +struct GoldenDriver final { + explicit GoldenDriver(VerifyingSlotProvider& verifier) noexcept + : verifier_(verifier) { + CipHeaderConfig config{}; + config.sid = 0; + config.dbs = kDbs; + config.fmt = 0x10; + config.fdf = 0x02; + cip_.Configure(config); + } + + bool EmitExplicit(bool isData, const Mutator& mutate = nullptr) noexcept { + TxPacketSlotView slot{}; + if (!verifier_.AcquireWritableSlot(index_, slot)) { + return false; + } + + const auto words = isData ? cip_.BuildData(dbc_, 0xFFFF) + : cip_.BuildNoData(dbc_); + WriteBE32(slot.bytes, words.q0); + WriteBE32(slot.bytes + 4, words.q1); + + const uint32_t payloadBytes = + isData ? kFramesPerData * kDbs * 4u : 0u; + for (uint32_t i = 0; i < payloadBytes; ++i) { + slot.bytes[8 + i] = 0; + } + + PreparedTxPacket packet{}; + packet.packetIndex = index_; + packet.byteCount = 8 + payloadBytes; + packet.isData = isData; + packet.dbc = dbc_; + packet.syt = 0xFFFF; + packet.firstAudioFrame = frame_; + packet.framesInPacket = isData ? kFramesPerData : 0; + packet.dbs = kDbs; + + if (mutate) { + mutate(packet, slot.bytes); + } + + verifier_.PublishSlot(packet); + + ++index_; + if (isData) { + dbc_ = static_cast(dbc_ + kFramesPerData); + frame_ += kFramesPerData; + } + return true; + } + + bool EmitAuto(const Mutator& mutate = nullptr) noexcept { + const bool isData = (pending_ + 6) >= 8; + if (!EmitExplicit(isData, mutate)) { + return false; + } + pending_ = static_cast(pending_ + 6 - (isData ? 8 : 0)); + return true; + } + + VerifyingSlotProvider& verifier_; + CipHeaderBuilder cip_{}; + uint32_t index_{0}; + uint8_t pending_{0}; + uint8_t dbc_{0}; + uint64_t frame_{0}; +}; + +// Runs warmup, one corrupted emit, cooldown; checks the targeted counter +// fired exactly `expected` times and nothing else did. +void RunSingleFaultCase(TestContext& ctx, VerifierCounterId targetId, + uint64_t expected, + const std::function& script) { + FakeIsochTxSlotProvider fake{}; + VerifyingSlotProvider verifier{fake}; + GoldenDriver driver{verifier}; + + for (int i = 0; i < 41; ++i) { + CHECK(ctx, driver.EmitAuto()); + } + + script(driver); + + for (int i = 0; i < 40; ++i) { + CHECK(ctx, driver.EmitAuto()); + } + + const VerifierSnapshot snapshot = verifier.Snapshot(); + CHECK_EQ_U64(ctx, snapshot.Value(targetId), expected); + CHECK_EQ_U64(ctx, snapshot.TotalViolations(), expected); + CHECK(ctx, snapshot.firstViolationValid); + CHECK_EQ_U32(ctx, snapshot.firstViolationId, + static_cast(targetId)); +} + +} // namespace + +void RunVerifyingSlotProviderTests(TestContext& ctx) { + // Clean 100k-cycle run: zero violations, exact N,D,D,D bookkeeping. + { + FakeIsochTxSlotProvider fake{}; + StickyCounterSink sink{}; + VerifyingSlotProvider verifier{ + fake, VerifyingSlotProvider::Config{true, 0x02, &sink}}; + GoldenDriver driver{verifier}; + + bool emitted = true; + for (int i = 0; i < 100000; ++i) { + emitted = driver.EmitAuto() && emitted; + } + CHECK(ctx, emitted); + + const VerifierSnapshot snapshot = verifier.Snapshot(); + CHECK_EQ_U64(ctx, snapshot.TotalViolations(), 0); + CHECK(ctx, !snapshot.firstViolationValid); + CHECK_EQ_U64(ctx, snapshot.Value(VerifierCounterId::kPacketsPublished), + 100000); + CHECK_EQ_U64(ctx, snapshot.Value(VerifierCounterId::kDataPackets), 75000); + CHECK_EQ_U64(ctx, snapshot.Value(VerifierCounterId::kNoDataPackets), + 25000); + CHECK_EQ_U64(ctx, snapshot.Value(VerifierCounterId::kAcquireCalls), + 100000); + CHECK_EQ_U64(ctx, snapshot.Value(VerifierCounterId::kAcquireFailures), 0); + + // The diag sink mirrors the counters one-for-one. + CHECK_EQ_U64(ctx, + sink.Value(static_cast( + VerifierCounterId::kPacketsPublished)), + 100000); + CHECK_EQ_U64(ctx, sink.OverflowedIds(), 0); + } + + // P2 — a data packet jumps the DBC (persistently, as a real bug would). + RunSingleFaultCase(ctx, VerifierCounterId::kP2DbcViolation, 1, + [](GoldenDriver& driver) { + driver.dbc_ = static_cast(driver.dbc_ + 4); + driver.EmitAuto(); + }); + + // P2 — a no-data packet advances the DBC it must carry unchanged. + RunSingleFaultCase(ctx, VerifierCounterId::kP2DbcViolation, 1, + [](GoldenDriver& driver) { + while ((driver.pending_ + 6) >= 8) { + driver.EmitAuto(); // reach the no-data cycle + } + driver.dbc_ = static_cast(driver.dbc_ + 8); + driver.EmitAuto(); + }); + + // P3 — data byte count disagrees with frames * dbs. + RunSingleFaultCase(ctx, VerifierCounterId::kP3ByteCountViolation, 1, + [](GoldenDriver& driver) { + while ((driver.pending_ + 6) < 8) { + driver.EmitAuto(); // reach a data cycle + } + driver.EmitAuto([](PreparedTxPacket& packet, uint8_t*) { + packet.byteCount -= 4; + }); + }); + + // P3 — no-data packet claims payload bytes (must be CIP-header-only). + RunSingleFaultCase(ctx, VerifierCounterId::kP3ByteCountViolation, 1, + [](GoldenDriver& driver) { + while ((driver.pending_ + 6) >= 8) { + driver.EmitAuto(); + } + driver.EmitAuto([](PreparedTxPacket& packet, uint8_t*) { + packet.byteCount = 16; + }); + }); + + // P3 — wrong FDF on the wire (Q1). + RunSingleFaultCase(ctx, VerifierCounterId::kP3CipQ1Violation, 1, + [](GoldenDriver& driver) { + while ((driver.pending_ + 6) < 8) { + driver.EmitAuto(); + } + driver.EmitAuto([](PreparedTxPacket&, uint8_t* bytes) { + bytes[5] = 0x03; // FDF byte of Q1 + }); + }); + + // P3 — wrong DBS field on the wire (Q0). + RunSingleFaultCase(ctx, VerifierCounterId::kP3CipQ0Violation, 1, + [](GoldenDriver& driver) { + driver.EmitAuto([](PreparedTxPacket&, uint8_t* bytes) { + bytes[1] = kDbs + 1; // DBS byte of Q0 + }); + }); + + // P4 — frame tiling gap (producer skipped 8 frames). + RunSingleFaultCase(ctx, VerifierCounterId::kP4FrameTilingViolation, 1, + [](GoldenDriver& driver) { + driver.frame_ += kFramesPerData; + driver.EmitAuto(); + }); + + // P4 — no-data packet claims frames (consistently-buggy producer). + RunSingleFaultCase(ctx, VerifierCounterId::kP4FrameCountViolation, 1, + [](GoldenDriver& driver) { + while ((driver.pending_ + 6) >= 8) { + driver.EmitAuto(); + } + driver.EmitAuto([](PreparedTxPacket& packet, uint8_t*) { + packet.framesInPacket = kFramesPerData; + }); + driver.frame_ += kFramesPerData; // producer believes it + }); + + // P1 — two consecutive no-data packets break the N,D,D,D run shape. + RunSingleFaultCase(ctx, VerifierCounterId::kP1CadenceRunViolation, 1, + [](GoldenDriver& driver) { + while ((driver.pending_ + 6) >= 8) { + driver.EmitAuto(); + } + driver.EmitAuto(); // the legitimate no-data + driver.EmitExplicit(false); // and an illegal twin + driver.pending_ = 2; // re-phase: next is D,D,D,N + }); + + // P1 — packet index gap. + RunSingleFaultCase(ctx, VerifierCounterId::kP1PacketIndexGapViolation, 1, + [](GoldenDriver& driver) { + driver.index_ += 1; + driver.EmitAuto(); + }); + + // P3 — publish without a matching acquire. + RunSingleFaultCase( + ctx, VerifierCounterId::kP3UnacquiredPublishViolation, 1, + [](GoldenDriver& driver) { + while ((driver.pending_ + 6) >= 8) { + driver.EmitAuto(); // park on a no-data cycle boundary + } + PreparedTxPacket packet{}; + packet.packetIndex = driver.index_; + packet.byteCount = 8; + packet.isData = false; + packet.dbc = driver.dbc_; + packet.syt = 0xFFFF; + packet.firstAudioFrame = driver.frame_; + packet.framesInPacket = 0; + packet.dbs = kDbs; + driver.verifier_.PublishSlot(packet); // never acquired + ++driver.index_; + driver.pending_ = + static_cast(driver.pending_ + 6); // it was the N slot + }); + + // P1 — tumbling 8000-packet window with one data packet short (runs stay + // legal: the tail is rescripted N,D,D,N instead of N,D,D,D). + { + FakeIsochTxSlotProvider fake{}; + VerifyingSlotProvider verifier{fake}; + GoldenDriver driver{verifier}; + + for (int i = 0; i < 7996; ++i) { + CHECK(ctx, driver.EmitAuto()); + } + CHECK(ctx, driver.EmitExplicit(false)); + CHECK(ctx, driver.EmitExplicit(true)); + CHECK(ctx, driver.EmitExplicit(true)); + CHECK(ctx, driver.EmitExplicit(false)); + + const VerifierSnapshot snapshot = verifier.Snapshot(); + CHECK_EQ_U64(ctx, snapshot.Value(VerifierCounterId::kPacketsPublished), + 8000); + CHECK_EQ_U64(ctx, snapshot.Value(VerifierCounterId::kDataPackets), 5999); + CHECK_EQ_U64( + ctx, snapshot.Value(VerifierCounterId::kP1CadenceWindowViolation), 1); + CHECK_EQ_U64(ctx, snapshot.TotalViolations(), 1); + } + + // Reset() clears counters and re-arms the learned state. + { + FakeIsochTxSlotProvider fake{}; + VerifyingSlotProvider verifier{fake}; + { + GoldenDriver driver{verifier}; + driver.index_ += 5; // guarantees an index-gap-free fresh start later + for (int i = 0; i < 20; ++i) { + CHECK(ctx, driver.EmitAuto()); + } + } + verifier.Reset(); + { + GoldenDriver driver{verifier}; + for (int i = 0; i < 20; ++i) { + CHECK(ctx, driver.EmitAuto()); + } + const VerifierSnapshot snapshot = verifier.Snapshot(); + CHECK_EQ_U64(ctx, snapshot.TotalViolations(), 0); + CHECK_EQ_U64(ctx, + snapshot.Value(VerifierCounterId::kPacketsPublished), 20); + } + } +} + +} // namespace ASFW::LabTests diff --git a/ADKVirtualAudioLab/Tests/main.cpp b/ADKVirtualAudioLab/Tests/main.cpp index 0ba23727..64fdd148 100644 --- a/ADKVirtualAudioLab/Tests/main.cpp +++ b/ADKVirtualAudioLab/Tests/main.cpp @@ -13,6 +13,8 @@ void RunPacketTimelineTests(TestContext& ctx); void RunPacketizerTests(TestContext& ctx); void RunPayloadWriterTests(TestContext& ctx); void RunDiceTxEngineTests(TestContext& ctx); +void RunVerifyingSlotProviderTests(TestContext& ctx); +void RunVerifierScenarioTests(TestContext& ctx); } // namespace ASFW::LabTests @@ -30,6 +32,8 @@ int main() { RunPacketizerTests(ctx); RunPayloadWriterTests(ctx); RunDiceTxEngineTests(ctx); + RunVerifyingSlotProviderTests(ctx); + RunVerifierScenarioTests(ctx); std::printf("%d checks, %d failures\n", ctx.checks, ctx.failures); return ctx.failures == 0 ? 0 : 1;