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|--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); });