Files
youle_cocos/.pnpm-store/v11/projects/f47659282495706053836086ad375b3e/scripts/test/build-game.test.mjs
T
2026-09-05 00:12:11 +08:00

148 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'node:path';
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync, mkdtempSync, symlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { platform } from 'node:process';
import { deflateSync } from 'node:zlib';
import { buildGame } from '../build-game.mjs';
const LINK_TYPE = platform === 'win32' ? 'junction' : 'dir';
function crc32(buf) {
let c = ~0;
for (const x of buf) { c ^= x; for (let i = 0; i < 8; i++) c = (c >>> 1) ^ (0xEDB88320 & -(c & 1)); }
return ~c >>> 0;
}
function chunk(type, data) {
const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
const td = Buffer.concat([Buffer.from(type, 'ascii'), data]);
const c = Buffer.alloc(4); c.writeUInt32BE(crc32(td));
return Buffer.concat([len, td, c]);
}
function makePng(w, h, rgb = [0, 0, 0]) {
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 2;
const raw = Buffer.alloc(h * (1 + w * 3));
for (let y = 0; y < h; y++) {
const off = y * (1 + w * 3);
raw[off] = 0;
for (let x = 0; x < w; x++) {
raw[off + 1 + x * 3] = rgb[0]; raw[off + 2 + x * 3] = rgb[1]; raw[off + 3 + x * 3] = rgb[2];
}
}
return Buffer.concat([
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw)), chunk('IEND', Buffer.alloc(0)),
]);
}
const DEFAULT_BTN = makePng(64, 64, [1, 2, 3]);
const SKINNED = makePng(64, 64, [9, 9, 9]);
function put(dir, rel, content) {
const p = join(dir, ...rel.split('/'));
mkdirSync(join(p, '..'), { recursive: true });
writeFileSync(p, content);
}
function scaffold(gameVersion = '3.8.8') {
const root = mkdtempSync(join(tmpdir(), 'youle-build-'));
const hostProject = join(root, 'nexus');
const frameworkSrc = join(hostProject, 'assets', 'framework');
mkdirSync(frameworkSrc, { recursive: true });
writeFileSync(join(hostProject, 'package.json'), JSON.stringify({ name: 'YouleNexus', creator: { version: '3.8.8' } }));
put(frameworkSrc, 'ui/atlas-hall/btn.png', DEFAULT_BTN);
// junction 故意指向一个诱饵目录而非 frameworkSrc 自身,与 materialize.test.mjs 同理:
// 否则"经 junction 拷来的字节"与"从真源拷来的字节"完全相同,测不出 Critical 1 那类 bug。
const frameworkDecoy = join(root, 'decoy', 'assets', 'framework');
mkdirSync(frameworkDecoy, { recursive: true });
put(frameworkDecoy, 'ui/atlas-hall/btn.png', DEFAULT_BTN);
put(frameworkDecoy, 'ui/atlas-hall/__decoy_marker__.png', DEFAULT_BTN);
const gamesDir = join(root, 'games');
const gameDir = join(gamesDir, 'demo');
mkdirSync(join(gameDir, 'assets'), { recursive: true });
writeFileSync(join(gameDir, 'package.json'), JSON.stringify({ name: 'demo', creator: { version: gameVersion } }));
symlinkSync(frameworkDecoy, join(gameDir, 'assets', 'framework'), LINK_TYPE);
return { root, hostProject, frameworkSrc, frameworkDecoy, gamesDir, gameDir, workspace: join(root, 'build-workspace') };
}
function cleanup(root) { rmSync(root, { recursive: true, force: true }); }
function baseOpts(s, extra = {}) {
return {
gamesDir: s.gamesDir, frameworkSrc: s.frameworkSrc, hostProject: s.hostProject,
workspace: s.workspace, platform: 'web-mobile',
frameworkCommit: 'abc1234', now: '2026-08-27T00:00:00.000Z',
runBuild: () => {},
...extra,
};
}
test('buildGame 走通全流程:实体化 → 合成 → 调构建 → 写 build-info', () => {
const s = scaffold();
try {
put(s.gameDir, 'assets/game/override/atlas-hall/btn.png', SKINNED);
const calls = [];
const r = buildGame('demo', baseOpts(s, { runBuild: (dir, p) => calls.push([dir, p]) }));
assert.ok(readFileSync(join(r.workspaceDir, 'assets', 'framework', 'ui', 'atlas-hall', 'btn.png')).equals(SKINNED));
assert.ok(readFileSync(join(s.frameworkSrc, 'ui', 'atlas-hall', 'btn.png')).equals(DEFAULT_BTN), '框架真源必须只读:合成只能发生在临时工作区');
assert.ok(!existsSync(join(r.workspaceDir, 'assets', 'framework', 'ui', 'atlas-hall', '__decoy_marker__.png')),
'framework 必须来自 frameworkSrc 真源,不能残留 junction 诱饵目标独有的文件(Critical 1)');
assert.equal(calls.length, 1);
assert.equal(calls[0][0], r.workspaceDir);
assert.equal(calls[0][1], 'web-mobile');
const info = JSON.parse(readFileSync(join(r.workspaceDir, 'build-info.json'), 'utf8'));
assert.equal(info.game, 'demo');
assert.equal(info.frameworkCommit, 'abc1234');
assert.equal(info.platform, 'web-mobile');
assert.deepEqual(info.skin, { matched: 1, frameworkTotal: 1 });
} finally { cleanup(s.root); }
});
test('buildGame 在引擎版本不一致时中止,且不调用构建', () => {
const s = scaffold('3.8.6');
try {
let called = false;
assert.throws(
() => buildGame('demo', baseOpts(s, { runBuild: () => { called = true; } })),
/引擎版本不一致/,
);
assert.equal(called, false);
} finally { cleanup(s.root); }
});
test('buildGame 在皮肤校验失败时中止,且不调用构建', () => {
const s = scaffold();
try {
put(s.gameDir, 'assets/game/override/atlas-hall/typo.png', 'X');
let called = false;
assert.throws(
() => buildGame('demo', baseOpts(s, { runBuild: () => { called = true; } })),
/皮肤覆盖校验未通过/,
);
assert.equal(called, false);
} finally { cleanup(s.root); }
});
test('buildGame 重复构建:清掉旧工作区后重建(幂等)', () => {
const s = scaffold();
try {
const opts = baseOpts(s);
buildGame('demo', opts);
const r2 = buildGame('demo', opts); // 不应因目标已存在而抛错
assert.ok(existsSync(join(r2.workspaceDir, 'build-info.json')));
} finally { cleanup(s.root); }
});
test('buildGame 对不存在的子游戏抛错', () => {
const s = scaffold();
try {
assert.throws(() => buildGame('nosuch', baseOpts(s)), /找不到子游戏工程/);
} finally { cleanup(s.root); }
});