mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-07 09:23:50 +01:00
* feat(test-free): --files-from runs exactly the listed planned files Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(pregate): declare scripts/ubicloud/pregate-remote.sh as gstack's remote gate (keyed Ubicloud VM, keyless local shards) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * perf(next-version): fetch PR VERSIONs concurrently; a timed-out fetch is an unknown claim Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(pregate): gstack remote-gate example and the small-machine pregate row Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(pregate): remote gate leaves paid test files to the paid gate, adds reason codes and the keyless backend line Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(next-version): unknown claims make the pick provisional (complete: false) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(pregate): remote-gate reason codes, paid-file handling and measured keyed/keyless timing Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(pregate): remote gate finishes before pregate's deadline pregate kills the remote gate at --timeout with a 5 s grace, which could cut a VM teardown short. pregate-remote.sh now reads the enclosing deadline (PREGATE_TIMEOUT_S, else the gstack-pregate --timeout it runs under, else 600), reserves 120 s for teardown plus 15 s margin, and tells the VM to stop at the earlier of that point and the cap's wall; a deadline that leaves under 300 s refuses before any VM (PREGATE_REMOTE_TIMEOUT_TOO_SHORT). It names the VM up front and prints the exact ubi-runner.sh down line, and the body is one braced block so an edit to the script mid-run cannot change what the run executes (keyed run 5 died after teardown with '$2: unbound variable' for that reason). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(restamp): an incomplete claim queue is never reported as read gstack-next-version now returns complete: false with unknown_claims when a PR's VERSION fetch timed out or failed. restamp's claim-at-open path took any usable JSON as 'read'; it now says 'incomplete', names the unknown PRs and calls the version provisional, the same warn-not-refuse stance as the existing 'unverified' fallback. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(pregate): only a gstack-pregate process sets the remote gate's deadline The ancestor walk matched any command line that mentioned gstack-pregate, so a shell line running pregate beside the tests (bash -c '...; bin/gstack-pregate ...') gave the stub gate a 600 s deadline. It now matches only a process whose program, or the script its interpreter runs, is gstack-pregate. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(changelog): A3 remote gate, --files-from, concurrent next-version Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * v1.91.132.0 chore: stamp Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
86 lines
5.3 KiB
TypeScript
86 lines
5.3 KiB
TypeScript
/**
|
|
* W2-A3 (fix wave 3): test-free-shards.ts --files-from <path> plans exactly
|
|
* the listed files from the free planned set and rejects, before planning, an
|
|
* unknown path, an empty list or a malformed line. The remote gate
|
|
* (scripts/ubicloud/pregate-remote.sh) compares the printed digest with its
|
|
* upload; the full run's FILES-FROM RESULT line is covered there.
|
|
*/
|
|
import { afterAll, describe, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { collectFreeTestFiles, filesDigest, parseCliOptions, readFilesFrom } from '../scripts/test-free-shards';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'free-files-from-'));
|
|
afterAll(() => fs.rmSync(DIR, { recursive: true, force: true }));
|
|
let n = 0;
|
|
const list = (text: string) => {
|
|
const file = path.join(DIR, `list-${++n}.txt`);
|
|
fs.writeFileSync(file, text);
|
|
return file;
|
|
};
|
|
const PLANNED = ['browse/test/a.test.ts', 'test/b.test.ts', 'test/c.test.ts', 'test/d.test.ts', 'test/e.test.ts', 'test/f.test.ts', 'test/g.test.ts'];
|
|
|
|
describe('--files-from parsing', () => {
|
|
test('one path per line; blank lines and CRLF endings are accepted; order is kept', () => {
|
|
expect(readFilesFrom(list('test/c.test.ts\r\n\r\nbrowse/test/a.test.ts\n\n'), PLANNED)).toEqual(['test/c.test.ts', 'browse/test/a.test.ts']);
|
|
});
|
|
|
|
test('paths outside the free planned set are rejected with the first five, a count and the --list fix', () => {
|
|
const file = list(['test/b.test.ts', ...['x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7'].map(name => `test/${name}.test.ts`)].join('\n'));
|
|
expect(() => readFilesFrom(file, PLANNED)).toThrow(
|
|
`--files-from ${file}: 7 path(s) not in the free planned set: test/x1.test.ts, test/x2.test.ts, test/x3.test.ts, test/x4.test.ts, test/x5.test.ts (+2 more); fix: use paths from \`bun run scripts/test-free-shards.ts --list\``);
|
|
});
|
|
|
|
test('an empty list, an unreadable file and every malformed line shape are rejected', () => {
|
|
const empty = list('\n\r\n\n');
|
|
expect(() => readFilesFrom(empty, PLANNED)).toThrow(`--files-from ${empty}: the list is empty (blank lines are ignored); nothing to run`);
|
|
expect(() => readFilesFrom(path.join(DIR, 'absent.txt'), PLANNED)).toThrow('cannot read it (ENOENT)');
|
|
const bad = [' test/b.test.ts', 'test/c.test.ts ', './test/d.test.ts', 'test/../test/e.test.ts', '/abs/test/f.test.ts', 'test\\g.test.ts', 'C:/x.test.ts', 'test//b.test.ts'];
|
|
for (const line of bad) {
|
|
expect(() => readFilesFrom(list(`test/b.test.ts\n${line}\n`), PLANNED), line).toThrow(`1 malformed line(s): line 2 ${JSON.stringify(line)}; fix: one repo-relative path per line`);
|
|
}
|
|
expect(() => readFilesFrom(list('test/b.test.ts\ntest/c.test.ts\ntest/b.test.ts\n'), PLANNED)).toThrow('line 3 repeats test/b.test.ts');
|
|
});
|
|
|
|
test('the digest is the sorted list, so a reordered upload still matches', () => {
|
|
expect(filesDigest(['test/c.test.ts', 'browse/test/a.test.ts'])).toBe(filesDigest(['browse/test/a.test.ts', 'test/c.test.ts']));
|
|
expect(filesDigest(['test/c.test.ts'])).not.toBe(filesDigest(['test/c.test.ts', 'test/b.test.ts']));
|
|
});
|
|
|
|
test('it conflicts with --quick, --windows-only, --list, --record-durations, --attribute-home and every --ci-* mode', () => {
|
|
expect(parseCliOptions(['--files-from', 'x.txt', '--dry-run']).filesFrom).toBe('x.txt');
|
|
expect(() => parseCliOptions(['--files-from'])).toThrow('Missing path for --files-from');
|
|
for (const extra of [['--quick'], ['--windows-only'], ['--list'], ['--record-durations'], ['--attribute-home'], ['--ci-plan', 'p.json'], ['--ci-run', 'p.json', '--shard', '1', '--result', 'r.json'], ['--ci-verify', 'p.json', '--results', 'r']]) {
|
|
expect(() => parseCliOptions(['--files-from', 'x.txt', ...extra]), extra[0]).toThrow('--files-from cannot be combined with --quick, --windows-only, --list, --record-durations, --attribute-home or the --ci-* modes');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('--files-from planning through the CLI', () => {
|
|
const cli = (...args: string[]) => {
|
|
const r = Bun.spawnSync([process.execPath, path.join(ROOT, 'scripts/test-free-shards.ts'), ...args], { cwd: ROOT, timeout: 60_000 });
|
|
return { exit: r.exitCode, out: r.stdout.toString() + r.stderr.toString() };
|
|
};
|
|
|
|
test('a dry run plans exactly the listed files and prints their digest', () => {
|
|
const planned = collectFreeTestFiles(ROOT);
|
|
const chosen = [planned[3]!, planned[0]!];
|
|
const file = list(chosen.join('\n') + '\n');
|
|
const r = cli('--files-from', file, '--dry-run');
|
|
expect(r.exit, r.out).toBe(0);
|
|
expect(r.out).toContain(`[test:free] files-from ${file}: 2 files, sha256=${filesDigest(chosen)}`);
|
|
expect(r.out).toContain('Would run 2 files across');
|
|
const listed = r.out.split('\n').filter(line => line.startsWith('Shard ') && line.includes('->')).flatMap(line => line.split('-> ')[1]!.split(', '));
|
|
expect(listed.sort()).toEqual([...chosen].sort());
|
|
}, 60_000);
|
|
|
|
test('a rejected list exits 1 before planning', () => {
|
|
const r = cli('--files-from', list('test/not-a-real-file.test.ts\n'), '--dry-run');
|
|
expect(r.exit).toBe(1);
|
|
expect(r.out).toContain('1 path(s) not in the free planned set: test/not-a-real-file.test.ts');
|
|
expect(r.out).not.toContain('Would run');
|
|
}, 60_000);
|
|
});
|