cosmonarchy-bw-prerelease/tools/tbl/test/TblText.test.ts
Kenny Udovic 36cc26b464 25 August 2026 -- tbl encoder decoder and image.tbl.txt loader
Ported from cosmonarchy-bw-release fe42f19. The .tbl.txt sources were
regenerated from this repo's own .tbl binaries rather than copied, so
images.tbl.txt and unitnames.tbl.txt carry the prerelease strings
(Askosi Sons of Ash, Faction Selection Askosi, Terran Fleshthief,
Forerunner Lumen) instead of the release ones.

tools/tbl is committed as real files here; in the release repo it is a
bare gitlink with no .gitmodules, so it comes out empty on clone.
2026-08-25 20:08:20 -04:00

120 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from "bun:test"
import * as TblText from "../src/TblText.ts"
import { entry, failWith, show, succeed } from "./helpers.ts"
describe("encodeEntry", () => {
it("leaves backslashes and ordinary ASCII alone", () => {
expect(TblText.encodeEntry(entry("zerg\\avenger.grp"))).toBe("zerg\\avenger.grp")
})
it("joins the parts of a multi-part entry with <0>", () => {
expect(TblText.encodeEntry(entry("Terran Maverick", "*", "Ground Units")))
.toBe("Terran Maverick<0>*<0>Ground Units")
})
it("escapes colour codes, newlines and high bytes as <N>", () => {
const bytes = Uint8Array.from([0x04, 0x45, 0x01, 0x0a, 0xa4])
expect(TblText.encodeEntry([bytes])).toBe("<4>E<1><10><164>")
})
it("escapes a literal < as <<", () => {
expect(TblText.encodeEntry(entry("a<b"))).toBe("a<<b")
})
it("marks a wholly empty entry with <>", () => {
expect(TblText.encodeEntry(entry(""))).toBe("<>")
expect(TblText.encodeEntry(entry("", ""))).toBe("<0>")
})
it("protects a leading # from being read back as a comment", () => {
expect(TblText.encodeEntry(entry("#1 unit"))).toBe("<35>1 unit")
})
it("protects leading and trailing spaces from editors that trim them", () => {
expect(TblText.encodeEntry(entry(" pad "))).toBe("<32>pad<32>")
expect(TblText.encodeEntry(entry(" "))).toBe("<32>")
})
})
describe("decodeEntry", () => {
const parse = (line: string) => succeed(TblText.decodeEntry(line, 1))
it("round-trips every construct the encoder emits", async () => {
const cases = [
entry("zerg\\avenger.grp"),
entry("Terran Maverick", "*", "Ground Units"),
entry(""),
entry("", ""),
entry("#1 unit"),
entry(" pad "),
entry("a<b"),
[Uint8Array.from([0x04, 0x45, 0x01, 0x0a, 0xa4])]
]
for (const original of cases) {
expect(show([await parse(TblText.encodeEntry(original))])).toEqual(show([original]))
}
})
it("treats <0> at the edges as empty parts", async () => {
expect(show([await parse("<0>a<0>")])).toEqual([["", "a", ""]])
})
it("reads <> as nothing anywhere in the line", async () => {
expect(show([await parse("a<>b")])).toEqual([["ab"]])
})
it("rejects an unterminated escape", async () => {
const error = await failWith(TblText.decodeEntry("abc<12", 7))
expect(error.message).toContain("unterminated escape")
expect(error.line).toBe(7)
expect(error.column).toBe(4)
})
it("rejects a non-numeric escape", async () => {
const error = await failWith(TblText.decodeEntry("a<nope>", 1))
expect(error.message).toContain("invalid escape `<nope>`")
expect(error.column).toBe(2)
})
it("rejects a byte above 255", async () => {
const error = await failWith(TblText.decodeEntry("<256>", 1))
expect(error.message).toContain("out of range")
})
it("rejects non-ASCII characters, which would be ambiguous bytes", async () => {
const error = await failWith(TblText.decodeEntry("café", 3))
expect(error.message).toContain("non-ASCII")
expect(error.message).toContain("U+00E9")
expect(error.column).toBe(4)
})
})
describe("decode", () => {
it("skips comments and blank lines", async () => {
const table = await succeed(TblText.decode("# a comment\n\nfirst\n\n# another\nsecond\n"))
expect(show(table)).toEqual([["first"], ["second"]])
})
it("accepts CRLF line endings and a UTF-8 BOM", async () => {
const table = await succeed(TblText.decode("# c\r\nfirst\r\nsecond\r\n"))
expect(show(table)).toEqual([["first"], ["second"]])
})
it("reports the real line number of a bad escape", async () => {
const error = await failWith(TblText.decode("# banner\nfine\nbroken<9z>\n"))
expect(error.line).toBe(3)
})
it("emits a banner by default and omits it when bare", () => {
const table = [entry("a")]
expect(TblText.encode(table)).toStartWith("# tbl-text v1")
expect(TblText.encode(table, { header: false })).toBe("a\n")
expect(TblText.encode(table, { header: false, annotate: true })).toBe("# [0]\na\n")
})
it("round-trips a whole table through the text form", async () => {
const table = [entry("first"), entry("a", "b"), entry(""), entry("# hash")]
expect(show(await succeed(TblText.decode(TblText.encode(table))))).toEqual(show(table))
})
})