Abfahrtsanzeiger Display Basic Library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
libmonoformat/ts/test/library.test.ts

334 lines
12 KiB

// =============================================================================
// library.test.ts — bun test suite for MonoDisplay library
//
// run: bun test
//
// Tests cover BinaryReader primitives and MonoDisplayParser with the real
// binary format: magic 0xAF7E2B63, section-based layout.
// =============================================================================
import { test, expect, describe } from "bun:test";
import {
BinaryReader,
MonoDisplayParser,
buildBinBuffer,
type StaticFrame,
type Animation,
type TextDisplay,
type ScrollText,
} from "../src/library";
// ---------------------------------------------------------------------------
// Low-level buffer helpers
// ---------------------------------------------------------------------------
// Real magic: 0xAF 0x7E 0x2B 0x63
const MAGIC_BYTES = new Uint8Array([0xAF, 0x7E, 0x2B, 0x63]);
function concat(...parts: (Uint8Array | ArrayBuffer)[]): ArrayBuffer {
const arrays = parts.map(p => p instanceof ArrayBuffer ? new Uint8Array(p) : p);
const total = arrays.reduce((s, a) => s + a.byteLength, 0);
const out = new Uint8Array(total);
let off = 0;
for (const a of arrays) { out.set(a, off); off += a.byteLength; }
return out.buffer;
}
function u8(n: number) { return new Uint8Array([n]); }
function u16le(n: number) { const b = new Uint8Array(2); new DataView(b.buffer).setUint16(0, n, true); return b; }
function u24le(n: number) { return new Uint8Array([n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF]); }
function u32le(n: number) { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, n, true); return b; }
/**
* Build a well-formed file buffer with a single section.
* sectionType: 0x01=static 0x02=animation 0x03=text 0x04=scrolltext
* sectionData: raw section payload (after section header)
*/
function makeFile(sectionType: number, sectionData: Uint8Array): ArrayBuffer {
const fileHeader = concat(
MAGIC_BYTES, // magic
u32le(1), // version = 1
u16le(1), // numSections = 1
u16le(0), // reserved = 0
);
const sectionHeader = concat(
u8(sectionType),
u24le(sectionData.byteLength),
);
return concat(fileHeader, sectionHeader, sectionData);
}
// ---------------------------------------------------------------------------
// BinaryReader
// ---------------------------------------------------------------------------
describe("BinaryReader", () => {
test("readByte", () => {
const r = new BinaryReader(new Uint8Array([0xAB]).buffer);
expect(r.readByte()).toBe(0xAB);
expect(r.remaining).toBe(0);
});
test("readUint16 LE", () => {
const r = new BinaryReader(new Uint8Array([0x34, 0x12]).buffer);
expect(r.readUint16()).toBe(0x1234);
});
test("readShort alias", () => {
const r = new BinaryReader(new Uint8Array([0x01, 0x00]).buffer);
expect(r.readShort()).toBe(1);
});
test("readUint24 LE", () => {
// 0x010203 LE → bytes [0x03, 0x02, 0x01]
const r = new BinaryReader(new Uint8Array([0x03, 0x02, 0x01]).buffer);
expect(r.readUint24()).toBe(0x010203);
});
test("readUint32 LE", () => {
const r = new BinaryReader(new Uint8Array([0x78, 0x56, 0x34, 0x12]).buffer);
expect(r.readUint32()).toBe(0x12345678);
});
test("readUint64 LE", () => {
const bytes = new Uint8Array(8);
new DataView(bytes.buffer).setBigUint64(0, 0x0102030405060708n, true);
const r = new BinaryReader(bytes.buffer);
expect(r.readUint64()).toBe(0x0102030405060708n);
});
test("offset advances through mixed reads", () => {
const r = new BinaryReader(new Uint8Array([1, 2, 3, 4, 5, 6]).buffer);
r.readByte(); expect(r.offset).toBe(1);
r.readUint16(); expect(r.offset).toBe(3);
expect(() => r.readUint32()).toThrow(RangeError); // only 3 remain
});
test("RangeError on exhaustion", () => {
const r = new BinaryReader(new Uint8Array([0x01]).buffer);
r.readByte();
expect(() => r.readByte()).toThrow(RangeError);
});
test("seek", () => {
const r = new BinaryReader(new Uint8Array([10, 20, 30]).buffer);
r.seek(2);
expect(r.readByte()).toBe(30);
});
test("seek out of bounds throws", () => {
const r = new BinaryReader(new Uint8Array([1, 2]).buffer);
expect(() => r.seek(5)).toThrow(RangeError);
});
test("readUtf8 ASCII", () => {
const enc = new TextEncoder().encode("hello");
const r = new BinaryReader(enc.buffer);
expect(r.readUtf8(5)).toBe("hello");
});
test("readUtf8 multi-byte codepoints", () => {
const str = "日本語🚀";
const enc = new TextEncoder().encode(str);
const r = new BinaryReader(enc.buffer);
expect(r.readUtf8(enc.byteLength)).toBe(str);
});
});
// ---------------------------------------------------------------------------
// MonoDisplayParser — header validation
// ---------------------------------------------------------------------------
describe("MonoDisplayParser — header", () => {
const parser = new MonoDisplayParser();
test("rejects wrong magic", () => {
const bad = new Uint8Array(16).fill(0);
bad[0] = 0xDE; bad[1] = 0xAD; bad[2] = 0xBE; bad[3] = 0xEF;
expect(() => parser.parse(bad.buffer)).toThrow(/magic/i);
});
test("rejects version 0 (illegal)", () => {
const buf = concat(MAGIC_BYTES, u32le(0), u16le(0), u16le(0));
expect(() => parser.parse(buf)).toThrow(/version/i);
});
test("rejects version > 1 (reserved)", () => {
const buf = concat(MAGIC_BYTES, u32le(2), u16le(0), u16le(0));
expect(() => parser.parse(buf)).toThrow(/version/i);
});
test("rejects 0 sections", () => {
const buf = concat(MAGIC_BYTES, u32le(1), u16le(0), u16le(0)); // 0 sections
expect(() => parser.parse(buf)).toThrow(/0 section/i);
});
test("skips unknown section type, throws when no recognised section follows", () => {
// Section type 0xFF is unknown
const unknownData = new Uint8Array(4);
const fileHeader = concat(MAGIC_BYTES, u32le(1), u16le(1), u16le(0));
const secHeader = concat(u8(0xFF), u24le(unknownData.byteLength));
const buf = concat(fileHeader, secHeader, unknownData);
expect(() => parser.parse(buf)).toThrow(/no recognised/i);
});
});
// ---------------------------------------------------------------------------
// MonoDisplayParser — static section
// ---------------------------------------------------------------------------
describe("MonoDisplayParser — static", () => {
const parser = new MonoDisplayParser();
test("parses 2×2 static frame", () => {
const data = concat(u16le(2), u16le(2), new Uint8Array([1, 0, 0, 1]));
const buf = makeFile(0x01, new Uint8Array(data));
const result = parser.parse(buf) as StaticFrame;
expect(result.type).toBe("static");
expect(result.width).toBe(2);
expect(result.height).toBe(2);
expect(result.pixels[0]).toBe(1);
expect(result.pixels[3]).toBe(1);
});
});
// ---------------------------------------------------------------------------
// MonoDisplayParser — animation section
// ---------------------------------------------------------------------------
describe("MonoDisplayParser — animation", () => {
const parser = new MonoDisplayParser();
test("parses 2-frame animation", () => {
const pixels = new Uint8Array(4); // 2×2
const data = concat(
u16le(2), u16le(2), // width, height
u16le(2), // frameCount
u32le(100), pixels, // frame 1: 100ms, all-off
u32le(200), new Uint8Array([1,1,1,1]), // frame 2: 200ms, all-on
);
const buf = makeFile(0x02, new Uint8Array(data));
const result = parser.parse(buf) as Animation;
expect(result.type).toBe("animation");
expect(result.frames.length).toBe(2);
expect(result.frames[0].durationMs).toBe(100);
expect(result.frames[1].durationMs).toBe(200);
});
test("empty animation (0 frames) is valid", () => {
const data = concat(u16le(4), u16le(4), u16le(0));
const buf = makeFile(0x02, new Uint8Array(data));
const result = parser.parse(buf) as Animation;
expect(result.frames.length).toBe(0);
});
});
// ---------------------------------------------------------------------------
// MonoDisplayParser — text section
// ---------------------------------------------------------------------------
describe("MonoDisplayParser — text", () => {
const parser = new MonoDisplayParser();
test("parses text with align=center", () => {
const enc = new TextEncoder().encode("HELLO");
const data = concat(u16le(enc.byteLength), enc, u8(0), u8(1)); // fontId=0 align=1
const buf = makeFile(0x03, new Uint8Array(data));
const result = parser.parse(buf) as TextDisplay;
expect(result.type).toBe("text");
expect(result.text).toBe("HELLO");
expect(result.align).toBe(1);
expect(result.fontId).toBe(0);
});
test("handles multi-byte UTF-8 text", () => {
const str = "日本語";
const enc = new TextEncoder().encode(str);
const data = concat(u16le(enc.byteLength), enc, u8(0), u8(0));
const buf = makeFile(0x03, new Uint8Array(data));
const result = parser.parse(buf) as TextDisplay;
expect(result.text).toBe(str);
});
});
// ---------------------------------------------------------------------------
// MonoDisplayParser — scrolltext section
// ---------------------------------------------------------------------------
describe("MonoDisplayParser — scrolltext", () => {
const parser = new MonoDisplayParser();
test("parses scrolltext left-direction", () => {
const enc = new TextEncoder().encode("SCROLL ME");
const data = concat(u16le(enc.byteLength), enc, u16le(60), u8(0));
const buf = makeFile(0x04, new Uint8Array(data));
const result = parser.parse(buf) as ScrollText;
expect(result.type).toBe("scrolltext");
expect(result.text).toBe("SCROLL ME");
expect(result.speedPps).toBe(60);
expect(result.direction).toBe(0);
});
test("handles multi-codepoint text incl. emoji", () => {
const str = "TICKER 🚀 日本語";
const enc = new TextEncoder().encode(str);
const data = concat(u16le(enc.byteLength), enc, u16le(80), u8(1));
const buf = makeFile(0x04, new Uint8Array(data));
const result = parser.parse(buf) as ScrollText;
expect(result.text).toBe(str);
expect(result.direction).toBe(1);
});
});
// ---------------------------------------------------------------------------
// MonoDisplayParser — unknown section skip
// ---------------------------------------------------------------------------
describe("MonoDisplayParser — section skip", () => {
const parser = new MonoDisplayParser();
test("skips unknown section, parses second known section", () => {
// File with 2 sections: 0xFF (unknown) then 0x01 (static 2×2)
const staticData = new Uint8Array(concat(u16le(2), u16le(2), new Uint8Array([0,1,1,0])));
const unknownData = new Uint8Array(4).fill(0xAA);
const fileHeader = concat(MAGIC_BYTES, u32le(1), u16le(2), u16le(0));
const sec1 = concat(u8(0xFF), u24le(unknownData.byteLength), unknownData);
const sec2 = concat(u8(0x01), u24le(staticData.byteLength), staticData);
const buf = concat(fileHeader, sec1, sec2);
const result = parser.parse(buf) as StaticFrame;
expect(result.type).toBe("static");
expect(result.pixels[1]).toBe(1);
});
});
// ---------------------------------------------------------------------------
// buildBinBuffer round-trip
// ---------------------------------------------------------------------------
describe("buildBinBuffer", () => {
const parser = new MonoDisplayParser();
test("static round-trip", () => {
const pixels = new Uint8Array([0, 1, 1, 0]);
const original: StaticFrame = { type: "static", width: 2, height: 2, pixels };
const buf = buildBinBuffer(original);
const parsed = parser.parse(buf) as StaticFrame;
expect(parsed.type).toBe("static");
expect(parsed.width).toBe(2);
expect(parsed.height).toBe(2);
expect(Array.from(parsed.pixels)).toEqual([0, 1, 1, 0]);
});
test("non-static throws (not implemented yet)", () => {
const anim: Animation = { type: "animation", width: 1, height: 1, frames: [] };
expect(() => buildBinBuffer(anim)).toThrow(/not yet implemented/);
});
});