mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 15:27:56 +01:00
fix(ci-carry): a file whose every test skipped on CI is not carried; tree.json records skipped_files (GST-1190)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
513115b889
commit
77f27fa083
8 files changed
+95
-17
No files matched your search
@@ -9,7 +9,10 @@
|
||||
- An unknown `.gstack/pregate.json` key under a `min_gstack_version` above the running gstack now says `needs gstack >= <version>; upgrade` (`PREGATE_GSTACK_TOO_OLD`). It still refuses.
|
||||
- docs/pregate.md#prove-a-mapping documents the proof route, the version floors, and that `tree.json` writers using Bun's JUnit must take the root `testsuite` count.
|
||||
|
||||
- A test file whose every test skipped on the CI runner no longer counts as carried (GST-1190, harness-evals: 75 tests). `tree.json` now lists such files in `skipped_files`, written by gstack's own Free Tests from the per-file JUnit counts; the pregate tests stage runs them here, and a /ship lane or guard whose inventory names one runs here. A partly skipped file still counts. An older `tree.json` without the list is read as before.
|
||||
|
||||
### Tests
|
||||
- `test/ship-ci-carry.test.ts`: in founderstar's shape (guards only, no lanes), `check-quick` carries from CI, `play-through` reuses a same-tree ledger pass, `assets` runs, and a dirty tree carries nothing. It fails without the guard carry.
|
||||
- `test/ship-ci-carry.test.ts`: a lane mapped by `jobs` carries when every named job passed at the same tree with no test files, and refuses for a missing job, a skipped job, a rerun whose newest attempt skipped the job, and another tree's run. Those five fail without the change.
|
||||
- `test/ship-ci-carry.test.ts` (GST-1190): an alias carries and an undeclared env prefix does not; a narrow lane reports its covered file count, not 9677; a jobs-mapped `bunx tsc --noEmit` guard carries with `n/a`; the newer-gstack message; `--simulate` writes and clears nothing and the branch's own map never carries; `carried` drops a row another base mapped. Six of these fail without the change.
|
||||
- `test/ci-tree-reuse.test.ts` and `test/ship-ci-carry.test.ts` (GST-1190 skipped files): the writer lists only fully skipped files and leaves the list out without complete counts; a fully skipped inventory file stops the lane carry, an empty list or older `tree.json` carries, and the tests stage runs only the skipped selected file. All three fail without the change.
|
||||
+15
-2
@@ -737,6 +737,17 @@ once:
|
||||
record (a reuse is never evidence). Its tree equals the tree git computes for
|
||||
the run's head commit. `files` is a list of paths and `tests` a
|
||||
non-negative integer when present; without `tests` the file count stands in.
|
||||
<a id="skipped-files"></a>`skipped_files`, when present, lists the files whose every test skipped on
|
||||
the runner (a Docker image, a drive path or an installed gstack it lacks;
|
||||
GST-1190). A carry treats them as uncovered: the pregate tests stage runs
|
||||
them here, and a /ship lane or guard whose inventory names one runs here
|
||||
whole. A partly skipped file ran tests and stays covered. gstack's own writer
|
||||
(`scripts/ci-tree-reuse.ts record`) derives the list from the shards' per-file
|
||||
JUnit counts and leaves it out when those counts are incomplete. A `tree.json`
|
||||
without the key (older writers, other repos) is read as before, with every
|
||||
listed file counted as run; add the list to your writer to close that gap.
|
||||
The remote gate's clearance does not use it: that gate runs on a Linux runner
|
||||
like CI, where the same files skip.
|
||||
- No run of the same workflow on that head, or on pregate.json's own head,
|
||||
whose newest attempt started at or after the second this run was created
|
||||
failed, was cancelled or is still running (`run_started_at`, so a red re-run
|
||||
@@ -895,7 +906,8 @@ whose workflow holds a lane this machine runs (gstack's `ci-reuse/free-tests`
|
||||
on Linux), with pregate.json's own head as a further veto head. Its tree must be this tree exactly (no
|
||||
stamp-only substitution). Then:
|
||||
|
||||
- The selected local test files the run's `tree.json` lists count as passed.
|
||||
- The selected local test files the run's `tree.json` lists count as passed,
|
||||
except those in its `skipped_files` ([above](#skipped-files)), which run here.
|
||||
The lanes check records them under `from_run` with the run receipt
|
||||
(`run tree=… tests=… runs=<id>/<attempt> profiles=… heads=… verified=…`) and
|
||||
prints `REUSE: accepted <n> of <m> selected local test file(s) from <receipt>;
|
||||
@@ -933,7 +945,8 @@ it accepts a run only when all of these hold:
|
||||
tree. A `pull_request` run counts only when the pull request already
|
||||
contained its base.
|
||||
- Every run executed tests, and with an inventory command, the runs'
|
||||
`tree.json` files cover every file it prints. A lane mapped by `jobs`
|
||||
`tree.json` files cover every file it prints, none of them a file whose every
|
||||
test skipped there (`skipped_files`). A lane mapped by `jobs`
|
||||
instead needs no test count: each named job succeeded in the run's newest
|
||||
attempt. Each run is read once more afterward (`run_changed`).
|
||||
|
||||
|
||||
+3
-2
@@ -86,8 +86,9 @@ export function findCarry(src: RunSource, o: { profiles: string[]; head: string;
|
||||
const listed = o.inventory();
|
||||
if ('error' in listed) return no('inventory_failed', listed.error);
|
||||
if (!listed.length) return no('inventory_empty', 'the inventory command printed no files');
|
||||
const missing = uncoveredFiles(listed, runs);
|
||||
if (missing.length) return no('inventory_uncovered', `${missing.length} of ${listed.length} inventory file(s) are in no run's tree.json: ${missing.slice(0, 5).join(', ')}${missing.length > 5 ? ', …' : ''}`);
|
||||
const missing = uncoveredFiles(listed, runs, 'uncovered');
|
||||
const skippedThere = missing.filter(f => runs.some(e => e.skipped?.includes(normalPath(f)))).length;
|
||||
if (missing.length) return no('inventory_uncovered', `${missing.length} of ${listed.length} inventory file(s) are in no run's tree.json${skippedThere ? ` or skipped every test there (${skippedThere} skipped_files)` : ''}: ${missing.slice(0, 5).join(', ')}${missing.length > 5 ? ', …' : ''}`);
|
||||
covered = new Set(listed.map(normalPath)).size;
|
||||
}
|
||||
for (const e of runs) {
|
||||
|
||||
@@ -40,7 +40,7 @@ import { selectLanes, type Selection } from '../lane-selection';
|
||||
import { launchRuntime } from '../runtime';
|
||||
import { cargoModel, type CargoModel } from '../rust';
|
||||
import { adapterFor, readRange, STALL_IDLE, supervise, type Counter, type Supervised } from '../supervise';
|
||||
import { verifyFromRun, type RunProfile } from '../../run-evidence';
|
||||
import { ranFiles, verifyFromRun, type RunProfile } from '../../run-evidence';
|
||||
import { matchesAny } from '../../ship-policy';
|
||||
import type { CheckDef, CheckResult, PregateContext, RemoteObligation } from '../types';
|
||||
|
||||
@@ -643,9 +643,10 @@ function fromRun(ctx: PregateContext, sel: Selection): { line: string; passed: s
|
||||
const names = 'error' in check ? [] : fromRunProfiles(ctx, check.profiles);
|
||||
const v = 'error' in check ? { ok: false as const, reason: 'profiles_unavailable', detail: check.error } : verifyFromRun(check, ref, names, ctx.wtree, ctx.head);
|
||||
if (!v.ok) return { line: `REUSE: refused reason=${v.reason} — ${v.detail}; every selected test runs here`, passed: [] };
|
||||
const ran = new Set(v.evidence.files);
|
||||
const ran = new Set(ranFiles(v.evidence));
|
||||
const passed = sel.localTests.filter(t => ran.has(t));
|
||||
return { line: `REUSE: accepted ${passed.length} of ${sel.localTests.length} selected local test file(s) from ${v.receipt}; ${sel.localTests.length - passed.length} still run`, passed, receipt: v.receipt };
|
||||
const skipped = sel.localTests.filter(t => v.evidence.skipped?.includes(t)).length;
|
||||
return { line: `REUSE: accepted ${passed.length} of ${sel.localTests.length} selected local test file(s) from ${v.receipt}; ${sel.localTests.length - passed.length} still run${skipped ? ` (${skipped} skipped every test in the run)` : ''}`, passed, receipt: v.receipt };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+15
-5
@@ -50,7 +50,7 @@ export interface ApiRun {
|
||||
repository?: { full_name: string } | null; head_repository?: { full_name: string } | null;
|
||||
}
|
||||
export interface ApiJob { name: string; conclusion: string | null; completed_at?: string | null }
|
||||
export interface TreeRecord { run_id?: unknown; run_attempt?: unknown; event?: unknown; sha?: unknown; head_sha?: unknown; tree?: unknown; reused?: unknown; reuse?: unknown; os?: unknown; files?: unknown; tests?: unknown }
|
||||
export interface TreeRecord { run_id?: unknown; run_attempt?: unknown; event?: unknown; sha?: unknown; head_sha?: unknown; tree?: unknown; reused?: unknown; reuse?: unknown; os?: unknown; files?: unknown; tests?: unknown; skipped_files?: unknown }
|
||||
|
||||
/** Everything verifyRun reads, injectable so every refusal has a fixture. */
|
||||
export interface RunDeps {
|
||||
@@ -64,7 +64,8 @@ export interface RunDeps {
|
||||
export type RunRefusal = 'run_ref_invalid' | 'other_repository' | 'fork_run' | 'workflow_mismatch' | 'event_mismatch' | 'run_in_progress'
|
||||
| 'run_not_success' | 'job_missing' | 'job_ambiguous' | 'job_skipped' | 'job_not_success' | 'artifact_missing' | 'artifact_mismatch'
|
||||
| 'artifact_reused' | 'tree_mismatch' | 'newer_run_vetoes' | 'run_changed' | 'run_stale' | 'os_mismatch' | 'api_error';
|
||||
export interface RunEvidence { run_id: number; run_attempt: number; head_sha: string; tree: string; profile: string; event: string; finished_at: string; files: string[]; tests: number | null }
|
||||
/** `skipped`: the files whose every test skipped on the runner (tree.json `skipped_files`, GST-1190); undefined when an older tree.json does not say. */
|
||||
export interface RunEvidence { run_id: number; run_attempt: number; head_sha: string; tree: string; profile: string; event: string; finished_at: string; files: string[]; tests: number | null; skipped?: string[] }
|
||||
export type RunVerdict = { ok: true; evidence: RunEvidence } | { ok: false; reason: RunRefusal; detail: string };
|
||||
|
||||
const SHA = /^[0-9a-f]{40}$/;
|
||||
@@ -187,11 +188,13 @@ export function verifyRun(deps: RunDeps, o: { repo: string; runRef: string; prof
|
||||
if (p.os && !oses.includes(p.os)) return fail('os_mismatch', `${at} ran on ${oses.filter(Boolean).map(String).join(', ') || 'an unrecorded OS'}; ${o.profileName} requires ${p.os}`);
|
||||
if (rec.files !== undefined && (!Array.isArray(rec.files) || rec.files.some(f => typeof f !== 'string'))) return fail('artifact_mismatch', `${at}'s ${p.artifact} files is not a list of paths`);
|
||||
if (rec.tests !== undefined && (!Number.isInteger(rec.tests) || (rec.tests as number) < 0)) return fail('artifact_mismatch', `${at}'s ${p.artifact} tests is not a non-negative integer`);
|
||||
if (rec.skipped_files !== undefined && (!Array.isArray(rec.skipped_files) || rec.skipped_files.some(f => typeof f !== 'string'))) return fail('artifact_mismatch', `${at}'s ${p.artifact} skipped_files is not a list of paths`);
|
||||
const files = ((rec.files ?? []) as string[]).map(normalPath);
|
||||
const tests = rec.tests === undefined ? null : (rec.tests as number);
|
||||
const changed = runChanged(deps, id, run.run_attempt);
|
||||
if (changed) return fail('run_changed', changed);
|
||||
return { ok: true, evidence: { run_id: id, run_attempt: run.run_attempt, head_sha: run.head_sha, tree: headTree, profile: o.profileName, event: run.event, finished_at: finishedAt!, files, tests } };
|
||||
const skipped = rec.skipped_files === undefined ? {} : { skipped: (rec.skipped_files as string[]).map(normalPath) };
|
||||
return { ok: true, evidence: { run_id: id, run_attempt: run.run_attempt, head_sha: run.head_sha, tree: headTree, profile: o.profileName, event: run.event, finished_at: finishedAt!, files, tests, ...skipped } };
|
||||
} catch (e: any) {
|
||||
return fail('api_error', `reading run ${id} failed: ${String(e?.message ?? e).slice(0, 300)}`);
|
||||
}
|
||||
@@ -211,8 +214,15 @@ export function runInventory(command: string, cwd: string, env?: Record<string,
|
||||
}
|
||||
|
||||
/** The files in `inventory` that no verified run's tree.json lists. */
|
||||
export function uncoveredFiles(inventory: string[], evidence: RunEvidence[]): string[] {
|
||||
const ran = new Set(evidence.flatMap(e => e.files.map(normalPath)));
|
||||
/** A run's files minus the ones whose every test skipped there (GST-1190): only those ran anything. Older tree.json: all files. */
|
||||
export const ranFiles = (e: RunEvidence): string[] => { const skipped = new Set(e.skipped ?? []); return e.files.map(normalPath).filter(f => !skipped.has(f)); };
|
||||
|
||||
/**
|
||||
* The inventory files no run covers. `skipped: 'uncovered'` (a carry, which drops tests on a machine where they might run)
|
||||
* also counts a file every test of which skipped in the run; `'covered'` (the remote gate, a Linux runner like CI) does not.
|
||||
*/
|
||||
export function uncoveredFiles(inventory: string[], evidence: RunEvidence[], skipped: 'uncovered' | 'covered' = 'covered'): string[] {
|
||||
const ran = new Set(evidence.flatMap(e => (skipped === 'uncovered' ? ranFiles(e) : e.files.map(normalPath))));
|
||||
return inventory.filter(f => !ran.has(normalPath(f)));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* The producer. A Free Tests or Windows Free Tests aggregate runs it only after its strict verification
|
||||
* passed, and uploads the file as the `tested-tree` artifact: run_id, run_attempt, event, sha (GITHUB_SHA),
|
||||
* head_sha (the pull request head for pull_request, else GITHUB_SHA), tree (git's tree of GITHUB_SHA),
|
||||
* workflow, os (the OSes the run's test jobs ran on), files (every test file in the plan) and tests.
|
||||
* workflow, os (the OSes the run's test jobs ran on), files (every test file in the plan), tests and, from complete
|
||||
* per-file counts, skipped_files (the planned files whose every test skipped on the runner; GST-1190).
|
||||
*
|
||||
* bun run scripts/ci-tree-reuse.ts find --workflow <file.yml> --profile <run_profiles name>
|
||||
* The main-push consumer (the `reuse` job). A push to main whose tree a pull request already tested green
|
||||
@@ -72,7 +73,8 @@ export function findReuse(deps: ReuseDeps, o: { repo: string; base: string; push
|
||||
}
|
||||
|
||||
/** tree.json for the `tested-tree` artifact; throws on any missing or malformed input, so a broken record fails the aggregate instead of uploading. */
|
||||
export function treeRecord(env: NodeJS.ProcessEnv, o: { plan: { shards: Array<{ files: string[] }> }; results: Array<{ outcome?: { summary?: { testsRan?: number } } }>; os: string[]; tree: string }) {
|
||||
type FileCounts = { tests: number; pass: number; fail: number };
|
||||
export function treeRecord(env: NodeJS.ProcessEnv, o: { plan: { shards: Array<{ files: string[] }> }; results: Array<{ outcome?: { summary?: { testsRan?: number }; counts?: { complete?: boolean; files?: Record<string, FileCounts> } } }>; os: string[]; tree: string }) {
|
||||
const int = (name: string) => { const v = Number(env[name]); if (!Number.isSafeInteger(v) || v < 1) throw new Error(`${name} is not a positive integer`); return v; };
|
||||
const sha = env.GITHUB_SHA ?? '';
|
||||
const event = env.GITHUB_EVENT_NAME ?? '';
|
||||
@@ -87,7 +89,11 @@ export function treeRecord(env: NodeJS.ProcessEnv, o: { plan: { shards: Array<{
|
||||
if (!files.length) throw new Error('the plan lists no test files');
|
||||
const counts = o.results.map(r => r.outcome?.summary?.testsRan);
|
||||
const tests = o.results.length && counts.every(n => Number.isSafeInteger(n) && n! >= 0) ? counts.reduce((a, b) => a! + b!, 0)! : files.length;
|
||||
return { run_id: int('GITHUB_RUN_ID'), run_attempt: int('GITHUB_RUN_ATTEMPT'), event, sha, head_sha: head, tree: o.tree, workflow, os: o.os, files, tests };
|
||||
// skipped_files (GST-1190): planned files whose every test skipped on this runner, so a carry runs them where they might not
|
||||
// skip. A partly skipped file ran tests and stays covered. Only from complete per-file counts; otherwise left out (unknown).
|
||||
const counted = o.results.length > 0 && o.results.every(r => r.outcome?.counts?.complete === true && r.outcome.counts.files);
|
||||
const skipped_files = counted ? files.filter(f => o.results.some(r => { const c = r.outcome!.counts!.files![f]; return !!c && c.tests > 0 && c.pass === 0 && c.fail === 0; })) : undefined;
|
||||
return { run_id: int('GITHUB_RUN_ID'), run_attempt: int('GITHUB_RUN_ATTEMPT'), event, sha, head_sha: head, tree: o.tree, workflow, os: o.os, files, tests, ...(skipped_files ? { skipped_files } : {}) };
|
||||
}
|
||||
|
||||
function flag(args: string[], name: string): string | undefined { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; }
|
||||
@@ -102,7 +108,7 @@ function record(args: string[]): number {
|
||||
const out = need(args, '--out');
|
||||
fs.mkdirSync(path.dirname(path.resolve(out)), { recursive: true });
|
||||
fs.writeFileSync(out, JSON.stringify(rec, null, 2) + '\n');
|
||||
console.log(`TESTED_TREE: run ${rec.run_id}/${rec.run_attempt} ${rec.event} head ${rec.head_sha.slice(0, 12)} tree ${rec.tree.slice(0, 12)} files=${rec.files.length} tests=${rec.tests} os=${rec.os.join(',')}`);
|
||||
console.log(`TESTED_TREE: run ${rec.run_id}/${rec.run_attempt} ${rec.event} head ${rec.head_sha.slice(0, 12)} tree ${rec.tree.slice(0, 12)} files=${rec.files.length} tests=${rec.tests} skipped_files=${rec.skipped_files?.length ?? 'unknown'} os=${rec.os.join(',')}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,19 @@ describe('treeRecord: the tested-tree producer', () => {
|
||||
expect(treeRecord(env({ GITHUB_EVENT_NAME: 'pull_request', GITHUB_SHA: MERGE, PR_HEAD_SHA: HEAD }), { plan, results, os: ['Windows'], tree: TREE })).toMatchObject({ event: 'pull_request', sha: MERGE, head_sha: HEAD });
|
||||
expect(treeRecord(env(), { plan, results: [{ outcome: {} }], os: ['Linux'], tree: TREE }).tests).toBe(3);
|
||||
});
|
||||
// Value: protects=a carry knows which files ran nothing on CI (GST-1190, harness-evals: 75 tests skipped on the runner); fails_when=a fully
|
||||
// skipped file is not listed, a partly skipped one is, or a run without complete counts claims a list
|
||||
test('skipped_files lists only files whose every test skipped; a partly skipped file stays covered; incomplete counts leave it out', () => {
|
||||
const n = (tests: number, pass: number, skip: number) => ({ tests, pass, fail: 0, skip, todo: 0, expects: pass });
|
||||
const counted = [
|
||||
{ outcome: { summary: { testsRan: 7 }, counts: { complete: true, files: { 'test/a.test.ts': n(3, 0, 3), 'test/b.test.ts': n(4, 2, 2) } } } },
|
||||
{ outcome: { summary: { testsRan: 5 }, counts: { complete: true, files: { 'test/c.test.ts': n(5, 5, 0) } } } },
|
||||
];
|
||||
expect(treeRecord(env(), { plan, results: counted, os: ['Linux'], tree: TREE }).skipped_files).toEqual(['test/a.test.ts']);
|
||||
expect(treeRecord(env(), { plan, results, os: ['Linux'], tree: TREE })).not.toHaveProperty('skipped_files');
|
||||
const partial = [counted[0]!, { outcome: { ...counted[1]!.outcome, counts: { complete: false, files: {} } } }];
|
||||
expect(treeRecord(env(), { plan, results: partial, os: ['Linux'], tree: TREE })).not.toHaveProperty('skipped_files');
|
||||
});
|
||||
test('missing or malformed inputs throw, so the aggregate fails instead of uploading a record', () => {
|
||||
expect(() => treeRecord(env({ GITHUB_EVENT_NAME: 'pull_request', GITHUB_SHA: MERGE }), { plan, results, os: ['Linux'], tree: TREE })).toThrow(/PR_HEAD_SHA/);
|
||||
expect(() => treeRecord(env({ GITHUB_RUN_ID: '' }), { plan, results, os: ['Linux'], tree: TREE })).toThrow(/GITHUB_RUN_ID/);
|
||||
|
||||
@@ -335,7 +335,7 @@ describe('GST-1190: aliases, newer-gstack keys, narrow counts, and proving a map
|
||||
const TSC = 'bunx tsc --noEmit';
|
||||
const f = shipFixture({ ...BASE, '.gstack/pregate.json': JSON.stringify({ run_profiles: { 'ci/free': PROFILE }, ship_lanes_from: { [TSC]: { profiles: ['ci/free'], jobs: ['typecheck'] } }, guards: [{ id: 'typecheck', when: ['src/**'], command: TSC, fix: 'bunx tsc --noEmit' }] }) + '\n' }); fixtures.push(f);
|
||||
world(f, { jobs: ALL, tree: { files: [], tests: 0 } });
|
||||
const g = f.run(path.join(BIN, 'gstack-pregate'), ['--stage', 'guards', '--allow-repo-commands', '--base', 'main']);
|
||||
const g = f.run(path.join(BIN, 'gstack-pregate'), ['--only', 'guards', '--allow-repo-commands', '--base', 'main']);
|
||||
expect([g.status, g.stdout]).toEqual([0, expect.stringContaining('GUARD: typecheck carried runs=7/1')]);
|
||||
const row = JSON.parse(fs.readFileSync(ledger(f)[0]!, 'utf8').trim().split('\n').at(-1)!);
|
||||
expect([row.tests_ran, row.jobs]).toEqual(['n/a', ['typecheck']]);
|
||||
@@ -380,3 +380,34 @@ describe('GST-1190: aliases, newer-gstack keys, narrow counts, and proving a map
|
||||
expect(JSON.parse(ev(f, ['carried', '--json']).stdout)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GST-1190: a file whose every test skipped on CI is not carried', () => {
|
||||
// Value: protects=a lane never carries a test file that ran nothing on the CI runner (Docker images, drive paths, an installed gstack), so
|
||||
// those tests run here instead of silently dropping; a partly skipped file and an older tree.json without skipped_files carry as before;
|
||||
// fails_when=harness-evals' 75 runner-skipped tests carry as passed; why_new=HARN-7 (harness-evals #240); seam=fake gh
|
||||
test('a fully skipped inventory file runs the lane here; a partly skipped one (not listed) and an older tree.json still carry', () => {
|
||||
const f = fx();
|
||||
world(f, { tree: { skipped_files: ['test/b.test.ts'] } });
|
||||
const rec = f.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]);
|
||||
expect([rec.status, rec.stderr]).toEqual([1, expect.stringMatching(/^CARRY_NONE: inventory_uncovered: 1 of 2 inventory file\(s\) are in no run's tree\.json or skipped every test there \(1 skipped_files\): test\/b\.test\.ts$/m)]);
|
||||
const out = preflight(f, [`free=${FREE}`]);
|
||||
expect(out.stdout).toContain('CI_CARRY: none label=free (inventory_uncovered');
|
||||
expect(runs(f, 'free-runs')).toBe(1);
|
||||
world(f, { tree: { skipped_files: [] } });
|
||||
expect(f.run(path.join(BIN, 'gstack-evidence'), ['record-ci', '--label', 'free', '--base', 'main', '--', FREE]).status).toBe(0);
|
||||
world(f, { tree: { skipped_files: 'test/b.test.ts' } });
|
||||
expect(f.run(path.join(BIN, 'gstack-ci-carry'), ['find', '--base', 'main', '--command', FREE, '--allow-repo-commands']).stdout).toContain('artifact_mismatch');
|
||||
});
|
||||
|
||||
test('the tests stage runs a selected file the run skipped entirely, and accepts the rest', () => {
|
||||
const conf = JSON.stringify({ runner: 'bun test {files}', dependencies: { 'src/**': ['test/a.test.ts', 'test/b.test.ts'] }, run_profiles: { 'ci/free': PROFILE } }) + '\n';
|
||||
const t = "import { test, expect } from 'bun:test';\ntest('x', () => expect(1).toBe(1));\n";
|
||||
const f = shipFixture({ ...BASE, '.gstack/pregate.json': conf, 'test/a.test.ts': t, 'test/b.test.ts': t }); fixtures.push(f);
|
||||
const stage = () => f.run(path.join(BIN, 'gstack-pregate'), ['--stage', 'tests', '--allow-repo-commands', '--base', 'main']);
|
||||
world(f, { tree: { skipped_files: ['test/b.test.ts'] } });
|
||||
const skipped = stage();
|
||||
expect(skipped.stdout + skipped.stderr).toMatch(/REUSE: accepted 1 of 2 selected local test file\(s\) from run .*; 1 still run \(1 skipped every test in the run\)/);
|
||||
world(f);
|
||||
expect(stage().stdout + stage().stderr).toMatch(/REUSE: accepted 2 of 2 selected local test file\(s\) from run .*; 0 still run/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user