Files
AptXmlAI/ra3apt-cli/test/public-cli-helper.cjs
T
lanyi e0b8dedc04 Fix APT CLI roundtrip visual and script preservation
Preserve edittext channels, alpha/color transforms, inactive placement fields,
zero ratio/clipping and null ClipAction presence. Keep unlabelled flag bits,
quoted/escaped script strings, export declarations and correctly typed CONST
and pushdata values. Validate signed integers and write byte-aligned UTF-8.

Add 23 public CLI regressions and record the native 174-resource batch plus
bounded retests: four discovered pages are repaired, while the sound placeholder
and native float formatting remain explicit accuracy limits. Rebuild the 1.0.1
Windows executable and leave unrelated SDK changes untouched.
2026-10-13 02:40:37 +02:00

119 lines
4.5 KiB
JavaScript

'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 = { '&amp;': '&', '&quot;': '"', '&apos;': "'", '&lt;': '<', '&gt;': '>' };
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(/<frame\b([^>]*)>([\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 };