Files
fpc-comet/tests/test_frame.pas
Ken Johnson 10452dd8fe Foundation: cm.types + cm.frame + test_frame
Two functional units land plus a unit test.  No tests/
infrastructure (run_tests.sh) or session/transfer logic
yet -- those follow in subsequent commits.

cm.types
--------
Pure constants + records, no I/O dependencies.  Lifts the
on-the-wire constants and types out of cometdef.pas (the
Comet daemon's omnibus def file) into a focused unit:

  - All NPKT_* packet-type byte codes from FSP-COMET-001
    section 6, including the LST trio (Q/R/S) and the
    KEYEX/KEYEXACK pair.
  - Internal pseudo-types (N_NOPKT / N_BADPKT / N_CARRIER
    / N_TIMEOUT) that the receive layer returns to signal
    control conditions.
  - All COPT_* capability flags from section 7.3, with
    COPT_ALL convenience mask.
  - Frame sizing (CM_FRAME_*), block sizing
    (CM_MIN/MAX/DEF_BLOCK_SIZE), window sizing,
    timeouts, adaptive-block parameters.
  - High-level enums for embedders: TCometDirection,
    TCometPhase (cmpInit through cmpDone), TCometAuthMethod
    (numerics matching the daemon's existing AUTH_* codes),
    TCometErrorCode, TCometSendFailReason,
    TCometReceiveDecision.
  - TCometRemoteInfo / TCometSessionStats /
    TCometSessionResult records that mirror the bp.types
    shape so consumers writing both BinkP and Comet code
    have the same mental model.

Imports TFTNAddress from fpc-msgbase (mb.address) -- single
source of truth for the FTN address layout across the
ecosystem, same approach fpc-binkp settled on.

cm.frame
--------
TStream-based frame I/O.  Implements FSP-COMET-001 section
5 wire format:

    LEN(4 LE) | TYPE(1) | SEQ(1) | PAYLOAD(N) | CRC32(4 LE)

CRC-32 covers TYPE + SEQ + PAYLOAD; LEN counts everything
after itself.  Inlined the CRC table (Ethernet / zip
polynomial 0xEDB88320) so the unit has zero external
dependencies on FPC's `crc` unit -- portable across all 7
target platforms with a tiny code-size cost.

Reader semantics match bp.frame: clean EOF at LEN
boundary -> False (peer closed cleanly), partial body ->
False with stream rewound so caller can retry on more
bytes, oversize header / CRC mismatch -> raise
ECometFrameError.

Frames carry their payload as TBytes (refcounted), so
unlike the cometfrm.pas source the parsed frame doesn't
need to be consumed before the next ReadFrame call.

test_frame
----------
21 checks across 6 cases:

  - empty payload (NPKT_IDLE) round-trip + 10-byte wire size
  - 16-byte payload round-trip + byte-exact match
  - 16 KB payload round-trip + size + bytes
  - half-frame returns False (not raise), stream rewound
  - bit-flip in payload raises ECometFrameError on CRC
  - CMPacketName mapping for known + unknown codes

All 21 pass.  Cross-platform clean on all 7 targets:
x86_64-linux, x86_64-freebsd, i386-go32v2, i386-os2,
i386-win32, i386-linux, i386-freebsd.
2026-04-22 09:57:03 -07:00

211 lines
4.9 KiB
ObjectPascal

{ test_frame -- pure TMemoryStream-driven exercise of cm.frame.
No sockets, no auth, no protocol state -- builds canned byte
sequences and asserts the parser decodes them correctly,
plus round-trips frames through the writer. }
program test_frame;
{$mode objfpc}{$H+}
{$modeswitch advancedrecords}
uses
Classes, SysUtils, cm.types, cm.frame;
var
TestsRun, TestsFailed: Integer;
procedure Check(Cond: Boolean; const Msg: string);
begin
Inc(TestsRun);
if not Cond then
begin
Inc(TestsFailed);
Writeln(' FAIL: ', Msg);
end;
end;
procedure TestEmptyPayload;
var
MS: TMemoryStream;
F1, F2: TCometFrame;
begin
Writeln('TestEmptyPayload');
MS := TMemoryStream.Create;
try
{ INIT with no payload (won't happen in real protocol but
exercises the zero-length payload path). }
F1.InitPayload(NPKT_IDLE, 0, F2 { dummy }, 0);
CMWriteFrame(MS, F1);
Check(MS.Size = 4 + 2 + 4,
'empty-payload frame is 10 bytes (LEN+TYPE+SEQ+CRC)');
MS.Position := 0;
Check(CMReadFrame(MS, F2), 'round-trip empty payload');
Check(F2.PktType = NPKT_IDLE, 'PktType preserved');
Check(F2.Seq = 0, 'Seq preserved');
Check(Length(F2.Payload) = 0, 'payload still empty');
finally
MS.Free;
end;
end;
procedure TestSmallPayload;
var
MS: TMemoryStream;
F1, F2: TCometFrame;
Body: array[0..15] of Byte;
I: Integer;
begin
Writeln('TestSmallPayload');
for I := 0 to 15 do Body[I] := Byte(I * 17 + 1);
MS := TMemoryStream.Create;
try
F1.InitPayload(NPKT_FINFO, 7, Body, 16);
CMWriteFrame(MS, F1);
MS.Position := 0;
Check(CMReadFrame(MS, F2), 'round-trip 16-byte payload');
Check(F2.PktType = NPKT_FINFO, 'PktType preserved');
Check(F2.Seq = 7, 'Seq preserved');
Check(Length(F2.Payload) = 16, 'payload length preserved');
Check(CompareByte(F2.Payload[0], Body[0], 16) = 0,
'payload bytes preserved');
finally
MS.Free;
end;
end;
procedure TestLargePayload;
var
MS: TMemoryStream;
F1, F2: TCometFrame;
Body: array[0..16383] of Byte;
I: Integer;
begin
Writeln('TestLargePayload');
for I := 0 to 16383 do Body[I] := Byte(I and $FF);
MS := TMemoryStream.Create;
try
F1.InitPayload(NPKT_DATA, 42, Body, 16384);
CMWriteFrame(MS, F1);
Check(MS.Size = 4 + 2 + 16384 + 4,
'16K-payload frame size = LEN + TYPE+SEQ + 16K + CRC');
MS.Position := 0;
Check(CMReadFrame(MS, F2), 'round-trip 16K payload');
Check(Length(F2.Payload) = 16384, 'large payload length');
Check(CompareByte(F2.Payload[0], Body[0], 16384) = 0,
'large payload bytes preserved');
finally
MS.Free;
end;
end;
procedure TestPartialReadReturnsFalse;
var
MS: TMemoryStream;
F1, F2: TCometFrame;
Body: array[0..7] of Byte;
I: Integer;
Wire: TBytes;
begin
Writeln('TestPartialReadReturnsFalse');
for I := 0 to 7 do Body[I] := Byte(I);
{ Build a complete frame in memory, then strip trailing
bytes so the reader sees a partial. }
MS := TMemoryStream.Create;
try
F1.InitPayload(NPKT_DATA, 1, Body, 8);
CMWriteFrame(MS, F1);
SetLength(Wire, MS.Size);
MS.Position := 0;
MS.Read(Wire[0], MS.Size);
finally
MS.Free;
end;
{ Truncate to half its bytes. }
MS := TMemoryStream.Create;
try
MS.WriteBuffer(Wire[0], Length(Wire) div 2);
MS.Position := 0;
Check(not CMReadFrame(MS, F2),
'half-frame returns False (not raise)');
Check(MS.Position = 0,
'partial-read rewinds for caller to retry');
finally
MS.Free;
end;
end;
procedure TestBadCRCRaises;
var
MS: TMemoryStream;
F1, F2: TCometFrame;
Body: array[0..7] of Byte;
I: Integer;
Raised: Boolean;
Wire: TBytes;
begin
Writeln('TestBadCRCRaises');
for I := 0 to 7 do Body[I] := Byte(I);
MS := TMemoryStream.Create;
try
F1.InitPayload(NPKT_DATA, 1, Body, 8);
CMWriteFrame(MS, F1);
SetLength(Wire, MS.Size);
MS.Position := 0;
MS.Read(Wire[0], MS.Size);
finally
MS.Free;
end;
{ Flip a bit in the payload -- CRC should fail. }
Wire[6] := Wire[6] xor $01;
MS := TMemoryStream.Create;
try
MS.WriteBuffer(Wire[0], Length(Wire));
MS.Position := 0;
Raised := False;
try
CMReadFrame(MS, F2);
except
on ECometFrameError do Raised := True;
end;
Check(Raised, 'corrupted payload raises ECometFrameError');
finally
MS.Free;
end;
end;
procedure TestPacketName;
begin
Writeln('TestPacketName');
Check(CMPacketName(NPKT_INIT) = 'NPKT_INIT', 'INIT name');
Check(CMPacketName(NPKT_DATA) = 'NPKT_DATA', 'DATA name');
Check(CMPacketName(NPKT_LSTREQ) = 'NPKT_LSTREQ', 'LSTREQ name');
Check(CMPacketName($AB) = 'NPKT_AB', 'unknown code fallback');
end;
begin
TestsRun := 0;
TestsFailed := 0;
TestEmptyPayload;
TestSmallPayload;
TestLargePayload;
TestPartialReadReturnsFalse;
TestBadCRCRaises;
TestPacketName;
Writeln;
Writeln(Format('%d tests run, %d failed', [TestsRun, TestsFailed]));
if TestsFailed > 0 then Halt(1);
end.