版本校验 → 实体化 → 皮肤合成 → 构建 → build-info。任一校验不过 即中止且不触发构建。runBuild 为注入点, 除真构建外全流程可单测。 Cocos 可执行路径只从 COCOS_CREATOR 环境变量取, 缺失显式报错。 同时补入 check-skin.mjs 的 process.argv[1] 守卫,与仓库其余脚本一致。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
138 lines
5.1 KiB
JavaScript
138 lines
5.1 KiB
JavaScript
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, [1, 2, 3]);
|
|
|
|
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);
|
|
|
|
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(frameworkSrc, join(gameDir, 'assets', 'framework'), LINK_TYPE);
|
|
|
|
return { root, hostProject, frameworkSrc, 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.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); }
|
|
});
|