Files
IR-protocol/tests/test_tx_contract.cpp

329 lines
13 KiB
C++

#include "IR_Encoder.h"
#include "IR_DecoderRaw.h"
#include <array>
#include <cassert>
#include <cstdint>
#include <iostream>
// Link-only seams for planner/lifecycle host tests. The real implementations
// are irrelevant here; no decoder or legacy sendByte helper is exercised.
bool IR_DecoderRaw::registerPairMuteEncoder(IR_Encoder *) { return true; }
void IR_DecoderRaw::refreshPairMuteState() {}
void IR_Encoder::send_HIGH(bool) {}
void IR_Encoder::send_LOW() {}
void IR_Encoder::send_EMPTY(uint8_t) {}
namespace
{
constexpr size_t kRunCapacity = 2048U;
constexpr size_t kFullMultiplyRunCapacity =
irproto::maxPhysicalGateRunCapacity(UINT16_MAX);
static_assert(irproto::kMaxLogicalTransmissionTicks == 25822U,
"golden maximum PHY duration changed");
static_assert(irproto::maxPhysicalTransmissionTicks(2U) == 25822U,
"nominal physical tick conversion changed");
static_assert(irproto::maxPhysicalGateRunCapacity(UINT8_MAX) <= 1024U,
"uint8_t carrier-multiply domain no longer fits the legacy Car allocation");
uint32_t sumTicks(const IrTxGateRun *runs, uint32_t count)
{
uint32_t total = 0U;
for (uint32_t i = 0; i < count; ++i)
total += runs[i].lenTicks;
return total;
}
void fillPattern(uint8_t *frame, uint8_t len, uint8_t pattern)
{
for (uint8_t i = 0; i < len; ++i)
{
switch (pattern)
{
case 0: frame[i] = 0x00U; break;
case 1: frame[i] = 0xFFU; break;
case 2: frame[i] = (i & 1U) ? 0x55U : 0xAAU; break;
default: frame[i] = static_cast<uint8_t>(i * 73U + 19U); break;
}
}
}
void testPlannerMatchesBuiltStream()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
std::array<IrTxGateRun, kRunCapacity> runs{};
const uint16_t multiplies[] = {2U, 3U, 6U};
for (uint16_t multiply : multiplies)
{
for (uint8_t len = 1U; len <= irproto::kMaxWireFrameBytes; ++len)
{
uint32_t durationForLength = 0U;
uint32_t ticksForLength = 0U;
for (uint8_t pattern = 0U; pattern < 4U; ++pattern)
{
fillPattern(frame.data(), len, pattern);
const IR_TxPlan planned =
IR_Encoder::planPhysicalTransmission(frame.data(), len, multiply);
const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
frame.data(), len, runs.data(), runs.size(), multiply);
assert(planned.valid());
assert(built.valid());
assert(planned.physicalTicks == built.physicalTicks);
assert(planned.gateRunCount == built.gateRunCount);
assert(planned.airtimeUs == built.airtimeUs);
assert(sumTicks(runs.data(), built.gateRunCount) == built.physicalTicks);
if (pattern == 0U)
{
durationForLength = planned.airtimeUs;
ticksForLength = planned.physicalTicks;
}
else
{
assert(planned.airtimeUs == durationForLength);
assert(planned.physicalTicks == ticksForLength);
}
}
}
}
}
void testGoldenNominalTimings()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
struct Golden { uint8_t bytes; uint32_t logicalTicks; uint32_t usCeil; };
const Golden golden[] = {
{6U, 5472U, 72000U},
{10U, 8728U, 114843U},
{31U, 25822U, 339764U},
};
for (const Golden& item : golden)
{
const IR_TxPlan plan =
IR_Encoder::planPhysicalTransmission(frame.data(), item.bytes, 2U);
assert(plan.valid());
assert(plan.physicalTicks == item.logicalTicks);
assert(plan.airtimeUs == item.usCeil);
assert(plan.airtimeMsCeil() == (item.usCeil + 999U) / 1000U);
}
}
void testCapacityAndClockContracts()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
std::array<IrTxGateRun, kRunCapacity> logicalRuns{};
fillPattern(frame.data(), frame.size(), 2U);
assert(IR_Encoder::buildGateRuns(
frame.data(), static_cast<uint8_t>(frame.size()),
logicalRuns.data(), logicalRuns.size()) != 0U);
std::array<uint8_t, irproto::kMaxWireFrameBytes + 1U> oversizedFrame{};
assert(IR_Encoder::buildGateRuns(
oversizedFrame.data(), static_cast<uint8_t>(oversizedFrame.size()),
logicalRuns.data(), logicalRuns.size()) == 0U);
const IR_TxPlan planned = IR_Encoder::planPhysicalTransmission(
frame.data(), static_cast<uint8_t>(frame.size()), 6U);
assert(planned.valid());
assert(planned.gateRunCount <= irproto::kIsrTxMaxGateRuns);
IrTxGateRun oneRun{};
const IR_TxPlan tooSmall = IR_Encoder::buildPhysicalTransmission(
frame.data(), static_cast<uint8_t>(frame.size()), &oneRun, 1U, 6U);
assert(!tooSmall.valid());
assert(tooSmall.status == IR_SendStatus::BuildGateRunsFailed);
assert(tooSmall.gateRunCount == planned.gateRunCount);
assert(tooSmall.physicalTicks == planned.physicalTicks);
std::array<uint8_t, 10U> tenBytes{};
IR_TxPlan configured = IR_Encoder::planPhysicalTransmission(
tenBytes.data(), static_cast<uint8_t>(tenBytes.size()), 6U);
assert(configured.airtimeUs == 114843U);
assert(IR_Encoder::applyTickClock(configured, 12000000U, 52U));
assert(configured.clockBasis == IR_TxClockBasis::ConfiguredTimer);
assert(configured.airtimeUs == 113464U);
assert(!IR_Encoder::planPhysicalTransmission(nullptr, 1U, 2U).valid());
assert(!IR_Encoder::planPhysicalTransmission(frame.data(), 0U, 2U).valid());
assert(!IR_Encoder::planPhysicalTransmission(
frame.data(), static_cast<uint8_t>(irproto::kMaxWireFrameBytes + 1U), 2U).valid());
}
void testDerivedFixedStorageCapacity()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
std::array<IrTxGateRun, kFullMultiplyRunCapacity> runs{};
const uint16_t multiplies[] = {2U, 3U, 6U, UINT8_MAX, UINT16_MAX};
for (uint16_t multiply : multiplies)
{
const size_t capacity = irproto::maxPhysicalGateRunCapacity(multiply);
for (uint8_t pattern = 0U; pattern < 4U; ++pattern)
{
fillPattern(frame.data(), static_cast<uint8_t>(frame.size()), pattern);
const IR_TxPlan planned = IR_Encoder::planPhysicalTransmission(
frame.data(), static_cast<uint8_t>(frame.size()), multiply);
const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
frame.data(), static_cast<uint8_t>(frame.size()),
runs.data(), capacity, multiply);
assert(planned.valid());
assert(built.valid());
assert(built.gateRunCount <= capacity);
assert(built.gateRunCount == planned.gateRunCount);
assert(built.physicalTicks == planned.physicalTicks);
assert(sumTicks(runs.data(), built.gateRunCount) == built.physicalTicks);
}
}
}
void testInPlacePhysicalScaling()
{
std::array<uint8_t, irproto::kMaxWireFrameBytes> frame{};
std::array<IrTxGateRun, kRunCapacity> logical{};
std::array<IrTxGateRun, kRunCapacity> expected{};
fillPattern(frame.data(), static_cast<uint8_t>(frame.size()), 3U);
const size_t logicalCount = IR_Encoder::buildGateRuns(
frame.data(), static_cast<uint8_t>(frame.size()),
logical.data(), logical.size());
assert(logicalCount != 0U);
for (uint16_t multiply : {2U, 3U, 6U})
{
auto scaled = logical;
size_t scaledCount = logicalCount;
const IR_TxPlan built = IR_Encoder::buildPhysicalTransmission(
frame.data(), static_cast<uint8_t>(frame.size()),
expected.data(), expected.size(), multiply);
assert(built.valid());
assert(IR_Encoder::scaleGateRunsToPhysical(
scaled.data(), &scaledCount, scaled.size(), multiply));
assert(scaledCount == built.gateRunCount);
for (size_t i = 0; i < scaledCount; ++i)
{
assert(scaled[i].gate == expected[i].gate);
assert(scaled[i].lenTicks == expected[i].lenTicks);
}
}
// Expansion beyond uint16_t is also in-place and preserves chunk order.
std::array<IrTxGateRun, 8U> longRun{};
longRun[0] = {65535U, true};
size_t longCount = 1U;
assert(IR_Encoder::scaleGateRunsToPhysical(
longRun.data(), &longCount, longRun.size(), 7U));
assert(longCount == 4U);
assert(longRun[0].lenTicks == 65535U);
assert(longRun[1].lenTicks == 65535U);
assert(longRun[2].lenTicks == 65535U);
assert(longRun[3].lenTicks == 32768U);
assert(sumTicks(longRun.data(), static_cast<uint32_t>(longCount)) == 229373U);
std::array<IrTxGateRun, 2U> tooSmall{{{65535U, true}, {1U, false}}};
size_t tooSmallCount = 1U;
assert(!IR_Encoder::scaleGateRunsToPhysical(
tooSmall.data(), &tooSmallCount, tooSmall.size(), 6U));
assert(tooSmallCount == 1U);
}
struct FakeBackend
{
IR_SendStatus startStatus = IR_SendStatus::Success;
bool finishSynchronously = false;
IR_Encoder *encoder = nullptr;
uint32_t operationId = 0U;
IR_TxPlan plan{};
};
IR_SendStatus fakeStart(void *opaque,
IR_Encoder *encoder,
const uint8_t *,
uint8_t,
const IR_TxPlan& plan,
uint32_t operationId)
{
auto& backend = *static_cast<FakeBackend *>(opaque);
backend.encoder = encoder;
backend.operationId = operationId;
backend.plan = plan;
if (backend.startStatus == IR_SendStatus::Success && backend.finishSynchronously)
encoder->externalFinishSend(operationId, IR_SendStatus::Success);
return backend.startStatus;
}
void testTokenLifecycle()
{
FakeBackend backend;
IR_Encoder::setExternalTxBackendV2(fakeStart, nullptr, &backend);
IR_Encoder encoder(1U, 42U, nullptr, false);
uint8_t payload = 0x5EU;
arduino_stub_micros = 100U;
const IR_SendResult first = encoder.sendData(IR_Broadcast, &payload, 1U);
assert(first.success);
assert(first.operationId != 0U);
assert(first.plannedAirtimeUs == backend.plan.airtimeUs);
assert(encoder.isBusy());
IR_TxSnapshot snapshot = encoder.txSnapshot();
assert(snapshot.operationId == first.operationId);
assert(snapshot.state == IR_TxState::Transmitting);
encoder.externalFinishSend(first.operationId + 1U, IR_SendStatus::Success);
assert(encoder.isBusy());
arduino_stub_micros = 200U;
encoder.externalFinishSend(first.operationId, IR_SendStatus::DmaTransferError);
snapshot = encoder.txSnapshot();
assert(!encoder.isBusy());
assert(snapshot.state == IR_TxState::Failed);
assert(snapshot.status == IR_SendStatus::DmaTransferError);
assert(snapshot.terminalAtUs == 200U);
encoder.externalFinishSend(first.operationId, IR_SendStatus::Success);
assert(encoder.txSnapshot().status == IR_SendStatus::DmaTransferError);
arduino_stub_micros = 300U;
const IR_SendResult second = encoder.sendData(IR_Broadcast, &payload, 1U);
assert(second.success && second.operationId != first.operationId);
encoder.externalFinishSend(first.operationId, IR_SendStatus::Success);
assert(encoder.isBusy());
encoder.externalFinishSend(second.operationId, IR_SendStatus::Success);
assert(encoder.isOperationComplete(second.operationId));
backend.startStatus = IR_SendStatus::DmaStartFailed;
const IR_SendResult rejectedAfterOwnership = encoder.sendData(IR_Broadcast, &payload, 1U);
assert(!rejectedAfterOwnership.success);
assert(rejectedAfterOwnership.operationId != 0U);
snapshot = encoder.txSnapshot();
assert(snapshot.state == IR_TxState::Failed);
assert(snapshot.status == IR_SendStatus::DmaStartFailed);
backend.startStatus = IR_SendStatus::Success;
const IR_SendResult active = encoder.sendData(IR_Broadcast, &payload, 1U);
const IR_SendResult busy = encoder.sendData(IR_Broadcast, &payload, 1U);
assert(active.success);
assert(!busy.success && busy.status == IR_SendStatus::EncoderBusy);
assert(busy.operationId == 0U);
encoder.externalFinishSend(active.operationId, IR_SendStatus::Success);
backend.finishSynchronously = true;
const IR_SendResult synchronous = encoder.sendData(IR_Broadcast, &payload, 1U);
assert(synchronous.success);
assert(encoder.isOperationComplete(synchronous.operationId));
assert(!encoder.isBusy());
IR_Encoder::setExternalTxBackendV2(nullptr, nullptr, nullptr);
}
} // namespace
int main()
{
testPlannerMatchesBuiltStream();
testGoldenNominalTimings();
testCapacityAndClockContracts();
testDerivedFixedStorageCapacity();
testInPlacePhysicalScaling();
testTokenLifecycle();
std::cout << "IR TX contract tests: OK\n";
return 0;
}