Files
Genarrative/scripts/ci-rust-cache.test.mjs
T
lhk229 705bb1e6b3
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
CI缓存自动维护与清理 (#462)
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/462
2026-09-22 17:19:11 +08:00

241 lines
7.7 KiB
JavaScript

import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { test } from 'node:test';
const script = resolve('scripts/ci-rust-cache.sh');
const linuxTest = process.platform === 'linux' ? test : test.skip;
function fixture(t) {
const directory = mkdtempSync(join(tmpdir(), 'ci-rust-cache-test-'));
const root = join(directory, 'snapshot');
const bin = join(directory, 'bin');
mkdirSync(join(root, 'objects'), { recursive: true });
mkdirSync(bin);
const envFile = join(directory, 'github-env');
writeFileSync(envFile, '');
writeFileSync(join(root, 'rustc.txt'), 'fixture rustc identity\n');
writeFileSync(join(root, 'source-commit.txt'), 'trusted-master-commit\n');
writeFileSync(join(root, 'workspace.txt'), `${process.cwd()}\n`);
writeFileSync(
join(bin, 'rustc'),
'#!/bin/bash\necho "fixture rustc identity"\n',
{ mode: 0o755 },
);
writeFileSync(
join(root, 'sccache'),
`#!/bin/bash
set -eu
case "$1" in
--stop-server) exit "\${STOP_FAILURE:-0}" ;;
--show-stats) exit 0 ;;
esac
if [[ "$*" == *-vV ]]; then
[[ "\${PROBE_FAILURE:-}" != 1 ]] || exit 1
exec "$@"
fi
printf '%s\\n' "\${SCCACHE_OSS_BUCKET-unset}" "\${SCCACHE_CONF}" "\${SCCACHE_DIR}" "\${SCCACHE_SERVER_UDS}" "\${SCCACHE_IGNORE_SERVER_IO_ERROR}" "\${SCCACHE_IDLE_TIMEOUT}" >> "\${TRACE}"
[[ "\${CACHE_FAILURE:-}" != 2 ]] || exit 2
exec "$@"
`,
{ mode: 0o755 },
);
const env = {
...process.env,
PATH: `${bin}:${process.env.PATH}`,
GITHUB_ENV: envFile,
GENARRATIVE_CI_RUST_CACHE_ROOT: root,
GENARRATIVE_CI_RUST_CACHE_STATE: '',
RUSTC_WRAPPER: 'bad-inherited-wrapper',
CARGO_BUILD_RUSTC_WRAPPER: 'bad-inherited-wrapper',
SCCACHE_OSS_BUCKET: 'must-not-use-publishing-cache',
TRACE: join(directory, 'trace'),
};
function run(args, extraEnv = {}) {
return spawnSync('bash', [script, ...args], {
env: { ...env, ...extraEnv },
encoding: 'utf8',
});
}
function preparedEnv() {
return Object.fromEntries(
readFileSync(envFile, 'utf8')
.trim()
.split('\n')
.map((line) => {
const separator = line.indexOf('=');
return [line.slice(0, separator), line.slice(separator + 1)];
}),
);
}
t.after(() => {
run(['report'], preparedEnv());
rmSync(directory, { recursive: true, force: true });
});
return { directory, root, env, run, preparedEnv };
}
linuxTest(
'missing snapshot and toolchain mismatch retain direct rustc',
(t) => {
const f = fixture(t);
const missing = f.run(['prepare'], {
GENARRATIVE_CI_RUST_CACHE_ROOT: join(f.directory, 'absent'),
});
assert.equal(missing.status, 0, missing.stderr);
assert.match(missing.stdout, /reason=snapshot-unavailable/);
assert.equal(f.preparedEnv().RUSTC_WRAPPER, '');
writeFileSync(join(f.root, 'rustc.txt'), 'different compiler\n');
const mismatch = f.run(['prepare']);
assert.equal(mismatch.status, 0, mismatch.stderr);
assert.match(mismatch.stdout, /reason=toolchain-mismatch/);
assert.equal(f.preparedEnv().CARGO_BUILD_RUSTC_WRAPPER, '');
writeFileSync(join(f.root, 'rustc.txt'), 'fixture rustc identity\n');
writeFileSync(join(f.root, 'workspace.txt'), '/different-checkout\n');
const moved = f.run(['prepare']);
assert.equal(moved.status, 0, moved.stderr);
assert.match(moved.stdout, /reason=workspace-mismatch/);
assert.equal(f.preparedEnv().RUSTC_WRAPPER, '');
},
);
linuxTest(
'preparations keep the compiler wrapper path stable with separate daemons',
(t) => {
const f = fixture(t);
assert.equal(f.run(['prepare']).status, 0);
const first = f.preparedEnv();
f.run(['report'], first);
assert.equal(f.run(['prepare']).status, 0);
const second = f.preparedEnv();
assert.equal(
first.CARGO_BUILD_RUSTC_WRAPPER,
second.CARGO_BUILD_RUSTC_WRAPPER,
);
assert.equal(first.RUSTC_WRAPPER, second.RUSTC_WRAPPER);
assert.notEqual(
first.GENARRATIVE_CI_RUST_CACHE_STATE,
second.GENARRATIVE_CI_RUST_CACHE_STATE,
);
},
);
linuxTest('failed wrapper probe falls back without enabling the cache', (t) => {
const f = fixture(t);
const result = f.run(['prepare'], { PROBE_FAILURE: '1' });
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /reason=wrapper-probe-failed/);
assert.equal(f.preparedEnv().RUSTC_WRAPPER, '');
assert.equal(f.preparedEnv().GENARRATIVE_CI_RUST_CACHE_STATE, '');
});
linuxTest(
'wrapper isolates remote settings and preserves compiler failures without retry',
(t) => {
const f = fixture(t);
const prepared = f.run(['prepare']);
assert.equal(prepared.status, 0, prepared.stderr);
const cachedEnv = f.preparedEnv();
const result = spawnSync(
cachedEnv.RUSTC_WRAPPER,
['/bin/bash', '-c', 'exit 42'],
{
env: { ...f.env, ...cachedEnv },
encoding: 'utf8',
},
);
assert.equal(result.status, 42, result.stderr);
const trace = readFileSync(f.env.TRACE, 'utf8').trim().split('\n');
assert.deepEqual(trace, [
'unset',
`${cachedEnv.GENARRATIVE_CI_RUST_CACHE_STATE}/config`,
`${f.root}/objects`,
`${cachedEnv.GENARRATIVE_CI_RUST_CACHE_STATE}/server.sock`,
'1',
'0',
]);
},
);
linuxTest(
'job copies use different cache directories and daemon sockets',
(t) => {
const first = fixture(t);
const second = fixture(t);
for (const f of [first, second]) {
const prepared = f.run(['prepare']);
assert.equal(prepared.status, 0, prepared.stderr);
const result = spawnSync(f.preparedEnv().RUSTC_WRAPPER, ['/bin/true'], {
env: { ...f.env, ...f.preparedEnv() },
encoding: 'utf8',
});
assert.equal(result.status, 0, result.stderr);
}
const a = readFileSync(first.env.TRACE, 'utf8').split('\n');
const b = readFileSync(second.env.TRACE, 'utf8').split('\n');
assert.notEqual(a[2], b[2]);
assert.notEqual(a[3], b[3]);
},
);
linuxTest(
'sccache infrastructure failure invokes rustc and preserves its result',
(t) => {
const f = fixture(t);
const prepared = f.run(['prepare']);
assert.equal(prepared.status, 0, prepared.stderr);
for (const [compiler, status] of [
['/bin/false', 1],
['/bin/true', 0],
]) {
const result = spawnSync(f.preparedEnv().RUSTC_WRAPPER, [compiler], {
env: { ...f.env, ...f.preparedEnv(), CACHE_FAILURE: '2' },
encoding: 'utf8',
});
assert.equal(result.status, status, result.stderr);
if (status === 1)
assert.match(result.stderr, /cache infrastructure failed/);
else assert.equal(result.stderr, '');
}
assert.equal(
readFileSync(f.env.TRACE, 'utf8').trim().split('\n').length,
6,
);
},
);
linuxTest('lost local cache state still invokes the real compiler', (t) => {
const f = fixture(t);
assert.equal(f.run(['/bin/bash', '-c', 'exit 43']).status, 43);
});
linuxTest('only a cleanly stopped master cache can export objects', (t) => {
const master = {
GITHUB_EVENT_NAME: 'push',
GITHUB_REF: 'refs/heads/master',
};
for (const stopFailure of ['0', '1']) {
const f = fixture(t);
const prepared = f.run(['prepare'], master);
assert.equal(prepared.status, 0, prepared.stderr);
const result = f.run(['report'], {
...f.preparedEnv(),
...master,
STOP_FAILURE: stopFailure,
});
assert.equal(result.status, 0, result.stderr);
assert.equal(
f.preparedEnv().GENARRATIVE_CI_RUST_CACHE_EXPORT_READY,
stopFailure === '0' ? '1' : '',
);
}
});