'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const marker = 'RA3_CLI_TEST_RAW_XML_ENVELOPE\n'; const cliRoot = path.resolve(process.env.RA3_APT_CLI_ROOT || path.join(__dirname, '..')); const runRoot = path.resolve(process.env.RA3_CLI_TEST_ARTIFACT_ROOT || path.join(cliRoot, '.work', 'tests')); function decodeAttribute(value) { return value.replace(/&(?:amp|quot|apos|lt|gt);|&#(?:x[0-9a-f]+|[0-9]+);/gi, item => { const named = { '&': '&', '"': '"', ''': "'", '<': '<', '>': '>' }; if (named[item] !== undefined) return named[item]; return String.fromCodePoint(item[2].toLowerCase() === 'x' ? parseInt(item.slice(3, -1), 16) : parseInt(item.slice(2, -1), 10)); }); } function attributes(tag) { const result = {}; for (const match of tag.matchAll(/([\w:-]+)="([^"]*)"/g)) { result[match[1]] = decodeAttribute(match[2]); } return result; } function element(xml, tag, attribute, value) { const tags = Array.from(xml.matchAll(new RegExp(String.raw`<${tag}\b(?:[^>"']|"[^"]*"|'[^']*')*>`, 'g')), match => attributes(match[0])); const found = tags.filter(attrs => attrs[attribute] === String(value)); assert.equal(found.length, 1, `one ${tag} ${attribute}=${value} must exist`); return found[0]; } function frame(xml, id) { const match = Array.from(xml.matchAll(/]*)>([\s\S]*?)<\/frame>/g)) .find(item => attributes(item[1]).id === String(id)); assert.ok(match, `frame ${id} must exist`); return match[2]; } function testCase(name) { fs.mkdirSync(runRoot, { recursive: true }); const directory = fs.mkdtempSync(path.join(runRoot, name + '-')); const temp = path.join(directory, 'temp'); const tools = path.join(directory, 'fake-tools'); fs.mkdirSync(temp); fs.mkdirSync(tools); return { directory, temp, tools }; } function cli(test, command, input, output) { const result = spawnSync(process.execPath, [ '--require', path.join(__dirname, 'fake-cc3tools.cjs'), path.join(cliRoot, 'bin', 'ra3apt.js'), command, input, '-o', output, '--tools-path', test.tools ], { encoding: 'utf8', cwd: test.directory, env: { ...process.env, TEMP: test.temp, TMP: test.temp, TMPDIR: test.temp } }); fs.writeFileSync(path.join(test.directory, command + '.log'), result.stdout + result.stderr); return result; } function succeeded(result) { assert.equal(result.error, undefined); assert.equal(result.status, 0, result.stderr || result.stdout); } function writeRaw(test, raw) { const input = path.join(test.directory, 'fixture.apt'); fs.writeFileSync(input, marker + raw); return input; } function rawOutput(output) { const envelope = fs.readFileSync(output, 'utf8'); assert.ok(envelope.startsWith(marker)); return envelope.slice(marker.length); } function expanded(test, raw) { const output = path.join(test.directory, 'fixture_edit.xml'); succeeded(cli(test, 'expand', writeRaw(test, raw), output)); return { file: output, xml: fs.readFileSync(output, 'utf8') }; } function build(test, human, stem = 'rebuilt') { const input = path.join(test.directory, stem + '_edit.xml'); const output = path.join(test.directory, stem + '.apt'); fs.writeFileSync(input, human); const result = cli(test, 'build', input, output); return { result, output, raw: result.status === 0 ? rawOutput(output) : null }; } function constRecords(output) { const table = fs.readFileSync(output.replace(/\.apt$/, '.const')); const count = table.readUInt32LE(0x18); const records = []; for (let i = 0; i < count; i++) { const offset = 0x20 + i * 8; const type = table.readUInt32LE(offset); const bits = table.readUInt32LE(offset + 4); assert.ok(type === 1 || type === 7, 'these fixtures use existing string/integer CONST encodings'); if (type === 1) { const end = table.indexOf(0, bits); assert.ok(end >= bits); records.push({ type, value: table.subarray(bits, end).toString('utf8'), offset: bits }); } else { records.push({ type, bits, value: table.readInt32LE(offset + 4) }); } } return records; } module.exports = { assert, fs, path, attributes, element, frame, testCase, cli, succeeded, writeRaw, rawOutput, expanded, build, constRecords, cliRoot };