@avi2dg/checks 0.72.0 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/docs/bins.md +1 -0
- package/docs/gates/checks-test-layout.md +1 -1
- package/docs/gates/checks-test.md +27 -5
- package/package.json +3 -2
- package/src/complexity/graphwalk.ts +2 -4
- package/src/core/git.ts +8 -4
- package/src/testing/changed-tests.ts +38 -0
- package/src/testing/test-skips.ts +6 -2
- package/src/testing/test.ts +39 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.73.0
|
|
6
|
+
|
|
7
|
+
Released 2026-11-07.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **testing:** run unit tier plus touched e2e files with checks-test --changed [#61](https://github.com/avi2d/checks/pull/61)
|
|
12
|
+
|
|
5
13
|
## 0.72.0
|
|
6
14
|
|
|
7
15
|
Released 2026-11-07.
|
package/docs/bins.md
CHANGED
|
@@ -44,6 +44,7 @@ A whole-tree-run workflow the repository writes runs them with `checks-lint --wh
|
|
|
44
44
|
These bins run on their own:
|
|
45
45
|
|
|
46
46
|
- [`checks-test`](gates/checks-test.md) runs the suite as `scripts.test` and refuses a skip without a reason at its test site.
|
|
47
|
+
With `--changed` it runs only the unit tier and the e2e test files a change touches.
|
|
47
48
|
- [`checks-flake`](gates/checks-flake.md) runs the suite when started by hand and records the seeds a flaky test fails with.
|
|
48
49
|
- [`checks-mutation`](gates/checks-mutation.md) runs Stryker for scoped checks and refuses a full run outside CI.
|
|
49
50
|
- [`checks-mutation-compare`](gates/checks-mutation-compare.md) compares the mutants of two reports and lists each one that regresses.
|
|
@@ -27,7 +27,7 @@ It fails unless the repository holds this shape, and names the file and the path
|
|
|
27
27
|
A test at the `e2e`, `live` or `pixel` level may do all of it, under `tests/quarantine/` as well.
|
|
28
28
|
Helpers in `tests/lib/**` answer to the same rule, since an in-process test reaches them.
|
|
29
29
|
- `scripts.test` is `checks-test`, optionally with `--parallel=<workers>`, which runs `bun test --randomize --parallel=<workers>` as [checks-test](checks-test.md#parallel-runs) says.
|
|
30
|
-
It refuses `--full`, so a local run stays capped.
|
|
30
|
+
It refuses `--full` and `--changed`, so a local run stays capped and CI still runs the suite it reaches.
|
|
31
31
|
- `scripts.lint` runs this check, itself or through `checks-lint` called by its bare bin name.
|
|
32
32
|
- `bunfig.toml` carries every `[test]` key of the shipped preset with the same value.
|
|
33
33
|
`[test].pathIgnorePatterns` is the preset's `["**/tests/quarantine/**", "**/tests/live/**", "**/tests/pixel/**", "repos/**"]`, which the check pins itself, so the kit's own repository, whose bunfig is the preset, cannot drift it either.
|
|
@@ -8,7 +8,7 @@ audience: consumers
|
|
|
8
8
|
|
|
9
9
|
## What it checks
|
|
10
10
|
|
|
11
|
-
It runs the unit tier plus the smoke list outside CI and the full suite under CI.
|
|
11
|
+
It runs the unit tier plus the smoke list outside CI and the full suite under CI, except a `--changed` run, as [Changed-file runs](#changed-file-runs) says.
|
|
12
12
|
It runs them with `bun test --randomize --parallel=<workers>`, as [Parallel runs](#parallel-runs) says, and reads Bun's JUnit report from that run.
|
|
13
13
|
It runs Bun with `CI=true`, so `test.only` fails the run in every environment.
|
|
14
14
|
Bun exits zero when tests skip, so `checks-test` checks every skipped test against its source declaration.
|
|
@@ -46,13 +46,14 @@ A skipped test or a todo without `skipReason` fails in every environment.
|
|
|
46
46
|
|
|
47
47
|
`checks-test` reads test source files and the JUnit report written by its own run.
|
|
48
48
|
A pull request run also reads the event file, the git history between the base and the head, the tracked sources, `package.json` and `bunfig.toml`.
|
|
49
|
+
A `--changed` run also reads the git history since the merge base with `origin/<default branch>`, the working tree diff and the untracked files, as [Changed-file runs](#changed-file-runs) says.
|
|
49
50
|
It reads no skip list from `package.json`, only the smoke list.
|
|
50
51
|
It counts a run as `ci` when `CI` is true and as `local` otherwise.
|
|
51
|
-
A local run covers `./tests/unit` plus the smoke list the repository declares.
|
|
52
|
+
A local run covers `./tests/unit` plus the smoke list the repository declares, except a `--changed` run, as [Changed-file runs](#changed-file-runs) says.
|
|
52
53
|
The smoke list lives in `package.json` under `checks.smoke` as an array of test file paths.
|
|
53
54
|
A repository with no smoke list runs the unit tier alone outside CI.
|
|
54
55
|
A local run with no unit test and no smoke list runs nothing and passes.
|
|
55
|
-
A `ci` run covers the full default suite, unless a pull request narrows it, as [Pull request runs](#pull-request-runs)
|
|
56
|
+
A `ci` run covers the full default suite, unless a pull request narrows it or `--changed` narrows it, as [Pull request runs](#pull-request-runs) and [Changed-file runs](#changed-file-runs) say.
|
|
56
57
|
A smoke entry that names no test file fails the run.
|
|
57
58
|
A smoke entry under `tests/quarantine/`, `tests/live/` or `tests/pixel/` fails the run, since Bun's ignored paths would drop it.
|
|
58
59
|
|
|
@@ -98,6 +99,25 @@ A run on any other event, such as `workflow_dispatch` for a release, runs the wh
|
|
|
98
99
|
`--full` and the live and pixel tiers always run whole.
|
|
99
100
|
A narrowed run judges only the skips the files it ran declare.
|
|
100
101
|
|
|
102
|
+
## Changed-file runs
|
|
103
|
+
|
|
104
|
+
`checks-test --changed` runs the unit tier and each e2e test file the change touches, so a desk run stays short.
|
|
105
|
+
The change is every path between where `HEAD` branched from `origin/<default branch>` and `HEAD`, plus the working tree's edits and its untracked files.
|
|
106
|
+
A deleted file drops out, and a renamed file counts under its new name.
|
|
107
|
+
An e2e test file is a `.test.ts` or `.test.tsx` file under `tests/e2e/`, so a helper under `tests/e2e/lib/` and a quarantined test never run.
|
|
108
|
+
The run names each e2e test file it picked, or says the change touches none and only the unit tier runs.
|
|
109
|
+
It judges the skips of the files it ran, as any other run does.
|
|
110
|
+
A clone with no remote-tracking default branch fails before any test runs and asks for a fetch of it.
|
|
111
|
+
A repository runs it from a script of its own, such as:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"scripts": {
|
|
116
|
+
"test:fast": "checks-test --changed"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
101
121
|
## Test tiers
|
|
102
122
|
|
|
103
123
|
Tests that need a live machine go in `tests/live/`, and tests that need a screen go in `tests/pixel/`.
|
|
@@ -146,9 +166,11 @@ A timings option waits for Bun to honour timings under random order.
|
|
|
146
166
|
|
|
147
167
|
## Arguments
|
|
148
168
|
|
|
149
|
-
The default command takes no arguments except `--parallel=<workers>`, as [Parallel runs](#parallel-runs) says, `--full` and `--junit=<file>`.
|
|
169
|
+
The default command takes no arguments except `--parallel=<workers>`, as [Parallel runs](#parallel-runs) says, `--full`, `--changed` and `--junit=<file>`.
|
|
150
170
|
`--full` runs the full suite outside CI, so a run started by hand covers what a local run skips.
|
|
151
171
|
A `package.json` script never takes `--full`, as [checks-test-layout](checks-test-layout.md) says of `scripts.test`.
|
|
172
|
+
`--changed` runs the unit tier and the e2e test files the change touches, as [Changed-file runs](#changed-file-runs) says, and takes neither a tier nor `--full`.
|
|
173
|
+
`scripts.test` never takes `--changed` either, so CI still runs the suite it reaches.
|
|
152
174
|
Its named tier options are `--tier=live`, `--tier=pixel`, `--tier=unit` and `--tier=e2e`, and a tier run takes `--parallel=<workers>` too, but not `--full`.
|
|
153
175
|
`--junit=<file>` keeps the JUnit report Bun wrote at `<file>`, whether the tests passed or failed, so a whole-tree run hands it to [checks-whole-tree-run](checks-whole-tree-run.md).
|
|
154
176
|
A `package.json` script never takes `--junit`, as [checks-test-layout](checks-test-layout.md) says of `scripts.test`.
|
|
@@ -161,7 +183,7 @@ It refuses test filters because every test excluded by a filter would look skipp
|
|
|
161
183
|
| --- | --- |
|
|
162
184
|
| 0 | every test passed and every skip has a matching site reason |
|
|
163
185
|
| 1 | a test failed, a parallel worker crashed, a skip lacks a site reason or a declaration is stale in CI |
|
|
164
|
-
| 2 | an argument is not one it takes, a declaration or the smoke list cannot be read, a smoke entry names no runnable test, `CI` is not a boolean
|
|
186
|
+
| 2 | an argument is not one it takes, a declaration or the smoke list cannot be read, a smoke entry names no runnable test, `CI` is not a boolean, Bun wrote no report or a `--changed` run cannot find where `HEAD` branched from `origin/<default branch>` |
|
|
165
187
|
|
|
166
188
|
## Sample output
|
|
167
189
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "avi2d",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"src/testing/test.ts",
|
|
56
56
|
"src/testing/test-skips.ts",
|
|
57
57
|
"src/testing/affected-tests.ts",
|
|
58
|
+
"src/testing/changed-tests.ts",
|
|
58
59
|
"src/core/swc.ts",
|
|
59
60
|
"src/testing/test-report.ts",
|
|
60
61
|
"src/testing/flake.ts",
|
|
@@ -221,7 +222,7 @@
|
|
|
221
222
|
"lint": "oxlint && bun src/core/lint.ts",
|
|
222
223
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
223
224
|
"test": "bun src/testing/test.ts",
|
|
224
|
-
"test:fast": "bun
|
|
225
|
+
"test:fast": "bun src/testing/test.ts --changed",
|
|
225
226
|
"test:unit": "bun src/testing/test.ts --tier=unit",
|
|
226
227
|
"test:e2e": "bun src/testing/test.ts --tier=e2e --parallel=4",
|
|
227
228
|
"test:live": "bun src/testing/test.ts --tier=live",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { BunServices } from "@effect/platform-bun";
|
|
3
3
|
import { Config, Console, Duration, Effect, FileSystem, Path, Schema } from "effect";
|
|
4
4
|
import { LINTED_SOURCE, TYPESCRIPT_SOURCE, type TrackedContent } from "../core/gates.ts";
|
|
5
|
-
import { checkoutFiles, collect, commitOf, git, pathsAt, rangeEnds, refArgs } from "../core/git.ts";
|
|
5
|
+
import { checkoutFiles, collect, commitOf, existingFiles, git, pathsAt, rangeEnds, refArgs } from "../core/git.ts";
|
|
6
6
|
import { PASSED, runExitMain, UNDECIDED, Usage, VIOLATED } from "../core/main.ts";
|
|
7
7
|
import { knipReport } from "./knip.ts";
|
|
8
8
|
|
|
@@ -233,10 +233,8 @@ const depcruise = Effect.fn("depcruise")(function* () {
|
|
|
233
233
|
});
|
|
234
234
|
|
|
235
235
|
const trackedInTree = Effect.fn("trackedInTree")(function* (root: string) {
|
|
236
|
-
const fs = yield* FileSystem.FileSystem;
|
|
237
|
-
const path = yield* Path.Path;
|
|
238
236
|
const listed = yield* git(["ls-files", "-z", "--", ...CRUISED], root);
|
|
239
|
-
return yield*
|
|
237
|
+
return yield* existingFiles(root, listed.split("\0").filter(Boolean));
|
|
240
238
|
});
|
|
241
239
|
|
|
242
240
|
const runSection = Effect.fn("runSection")(function* (
|
package/src/core/git.ts
CHANGED
|
@@ -32,13 +32,17 @@ export const collect = Effect.fn("collect")(
|
|
|
32
32
|
Effect.scoped,
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// Effect.filter with concurrency keeps the order its checks finish in, not the order it was given.
|
|
36
|
+
export const existingFiles = Effect.fn("existingFiles")(function* (root: string, files: readonly string[]) {
|
|
36
37
|
const fs = yield* FileSystem.FileSystem;
|
|
37
38
|
const path = yield* Path.Path;
|
|
39
|
+
const exists = yield* Effect.forEach(files, (file) => fs.exists(path.join(root, file)), { concurrency: "unbounded" });
|
|
40
|
+
return files.filter((_, index) => exists[index]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const workingTreeFiles = Effect.fn("workingTreeFiles")(function* (root: string) {
|
|
38
44
|
const listed = yield* git(["ls-files", "-z", "--cached", "--others", "--exclude-standard"], root);
|
|
39
|
-
return yield*
|
|
40
|
-
concurrency: "unbounded",
|
|
41
|
-
});
|
|
45
|
+
return yield* existingFiles(root, listed.split("\0").filter(Boolean));
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
export const git = Effect.fn("git")(function* (args: readonly string[], cwd?: string, feed?: Feed) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { changedPaths, defaultBranch, existingFiles, git, rangeEnds, type Change } from "../core/git.ts";
|
|
3
|
+
|
|
4
|
+
const E2E_PREFIX = "tests/e2e/";
|
|
5
|
+
const TEST_SUFFIXES = [".test.ts", ".test.tsx"] as const;
|
|
6
|
+
|
|
7
|
+
class BaseUnresolved extends Schema.TaggedError<BaseUnresolved>()("BaseUnresolved", {
|
|
8
|
+
message: Schema.String,
|
|
9
|
+
}) {}
|
|
10
|
+
|
|
11
|
+
export function keptPaths(changes: readonly Change[]): readonly string[] {
|
|
12
|
+
return changes.filter((change) => change.kind !== "deleted").map((change) => change.path);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function touchedE2eFiles(paths: readonly string[]): readonly string[] {
|
|
16
|
+
const picked = new Set<string>();
|
|
17
|
+
for (const path of paths) {
|
|
18
|
+
if (!path.startsWith(E2E_PREFIX)) continue;
|
|
19
|
+
if (!TEST_SUFFIXES.some((suffix) => path.endsWith(suffix))) continue;
|
|
20
|
+
picked.add(path);
|
|
21
|
+
}
|
|
22
|
+
return [...picked].sort();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const splitZero = (output: string): readonly string[] => output.split("\0").filter((line) => line !== "");
|
|
26
|
+
|
|
27
|
+
export const changedE2eFiles = Effect.fn("changedE2eFiles")(function* (root: string) {
|
|
28
|
+
const target = "origin/" + (yield* defaultBranch(root));
|
|
29
|
+
const range = yield* rangeEnds(target, "HEAD", root).pipe(
|
|
30
|
+
Effect.mapError((cause) => new BaseUnresolved({
|
|
31
|
+
message: "cannot find where HEAD branched from " + target + ", so the e2e files the change touches are unknown; fetch " + target + " and run again: " + cause.message,
|
|
32
|
+
})),
|
|
33
|
+
);
|
|
34
|
+
const committed = keptPaths(yield* changedPaths(range.base, range.head, [], root));
|
|
35
|
+
const working = splitZero(yield* git(["diff", "--name-only", "-z", "HEAD", "--"], root));
|
|
36
|
+
const untracked = splitZero(yield* git(["ls-files", "--others", "--exclude-standard", "-z"], root));
|
|
37
|
+
return yield* existingFiles(root, touchedE2eFiles([...committed, ...working, ...untracked]));
|
|
38
|
+
});
|
|
@@ -201,8 +201,12 @@ export function smokeFiles(scanned: readonly string[], smoke: readonly string[])
|
|
|
201
201
|
message: "checks.smoke cannot name a quarantined, live or pixel test at " + ignored.join(", "),
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
return { kind: "valid", files: unitTierPlus(scanned, smoke) };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function unitTierPlus(scanned: readonly string[], extra: readonly string[]): readonly string[] {
|
|
208
|
+
const wanted = new Set(extra);
|
|
209
|
+
return scanned.filter((file) => selectedTierFile(file, "unit") || wanted.has(file));
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
export const readSmokeDeclarations = Effect.fn("readSmokeDeclarations")(function* (
|
package/src/testing/test.ts
CHANGED
|
@@ -5,12 +5,14 @@ import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
|
5
5
|
import { TEST_ENTRY_POINT } from "../core/gates.ts";
|
|
6
6
|
import { runMain, Usage } from "../core/main.ts";
|
|
7
7
|
import { readPullRequestNarrowing } from "./affected-tests.ts";
|
|
8
|
+
import { changedE2eFiles } from "./changed-tests.ts";
|
|
8
9
|
import {
|
|
9
10
|
readDeclarationsIn,
|
|
10
11
|
readSkipDeclarations,
|
|
11
12
|
readSmokeDeclarations,
|
|
12
13
|
scanTestFiles,
|
|
13
14
|
selectedTierFile,
|
|
15
|
+
unitTierPlus,
|
|
14
16
|
type Environment,
|
|
15
17
|
type SkipDeclaration,
|
|
16
18
|
type TestTier,
|
|
@@ -38,8 +40,9 @@ export type TestOptions = {
|
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
const NAME = TEST_ENTRY_POINT.bin;
|
|
41
|
-
const USAGE = `usage: ${NAME} [--tier=live | --tier=pixel | --tier=unit | --tier=e2e] [--parallel=<workers>] [--
|
|
43
|
+
const USAGE = `usage: ${NAME} [--tier=live | --tier=pixel | --tier=unit | --tier=e2e | --full | --changed] [--parallel=<workers>] [--junit=<file>], and it refuses test filters`;
|
|
42
44
|
const FULL = "--full";
|
|
45
|
+
const CHANGED = "--changed";
|
|
43
46
|
const TIERS = new Map<string, TestTier>([
|
|
44
47
|
["--tier=live", "live"],
|
|
45
48
|
["--tier=pixel", "pixel"],
|
|
@@ -68,14 +71,20 @@ export function testOptions(args: readonly string[]): TestOptions | undefined {
|
|
|
68
71
|
return options;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
export type EntryOptions = {
|
|
74
|
+
export type EntryOptions = {
|
|
75
|
+
readonly options: TestOptions;
|
|
76
|
+
readonly junit: Option.Option<string>;
|
|
77
|
+
readonly changed: boolean;
|
|
78
|
+
};
|
|
72
79
|
|
|
73
80
|
// Kept out of testOptions, so checks-test-layout still holds a package.json test script to --parallel alone.
|
|
74
81
|
export function entryOptions(args: readonly string[]): EntryOptions | undefined {
|
|
75
82
|
const junit = args.flatMap((arg) => JUNIT.exec(arg)?.[1] ?? []);
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
const changed = args.filter((arg) => arg === CHANGED).length;
|
|
84
|
+
const options = testOptions(args.filter((arg) => !JUNIT.test(arg) && arg !== CHANGED));
|
|
85
|
+
if (options === undefined || junit.length > 1 || changed > 1) return undefined;
|
|
86
|
+
if (changed === 1 && (options.tier !== undefined || options.full === true)) return undefined;
|
|
87
|
+
return { options, junit: Option.fromNullishOr(junit[0]), changed: changed === 1 };
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
const TEST_SCRIPTS: readonly string[] = [NAME, `bun src/${TEST_ENTRY_POINT.script}`];
|
|
@@ -176,7 +185,8 @@ export type SuiteSelection =
|
|
|
176
185
|
| { readonly kind: "full" }
|
|
177
186
|
| { readonly kind: "tier"; readonly tier: TestTier }
|
|
178
187
|
| { readonly kind: "smoke" }
|
|
179
|
-
| { readonly kind: "files"; readonly files: readonly string[] }
|
|
188
|
+
| { readonly kind: "files"; readonly files: readonly string[] }
|
|
189
|
+
| { readonly kind: "changed"; readonly e2e: readonly string[] };
|
|
180
190
|
|
|
181
191
|
export function selectSuite(options: TestOptions, environment: Environment): SuiteSelection {
|
|
182
192
|
if (options.tier !== undefined) return { kind: "tier", tier: options.tier };
|
|
@@ -195,6 +205,7 @@ export function smokeList(manifest: unknown): readonly string[] | undefined {
|
|
|
195
205
|
|
|
196
206
|
export function suiteArgs(selection: SuiteSelection, smoke: readonly string[]): readonly string[] {
|
|
197
207
|
if (selection.kind === "tier") return tierArgs(selection.tier);
|
|
208
|
+
if (selection.kind === "changed") return suiteArgs({ kind: "smoke" }, selection.e2e);
|
|
198
209
|
if (selection.kind === "smoke") return ["./tests/unit", ...smoke.map((file) => `./${file}`)];
|
|
199
210
|
if (selection.kind === "files") return selection.files.map((file) => `./${file}`);
|
|
200
211
|
return [];
|
|
@@ -248,6 +259,10 @@ const narrowSelection = Effect.fn("narrowSelection")(function* (options: TestOpt
|
|
|
248
259
|
|
|
249
260
|
const readScope = Effect.fn("readScope")(function* (selection: SuiteSelection, smoke: readonly string[]) {
|
|
250
261
|
if (selection.kind === "smoke") return yield* readSmokeDeclarations(process.cwd(), smoke);
|
|
262
|
+
if (selection.kind === "changed") {
|
|
263
|
+
const files = unitTierPlus(yield* scanTestFiles(process.cwd()), selection.e2e);
|
|
264
|
+
return { files, declarations: yield* readDeclarationsIn(process.cwd(), files) };
|
|
265
|
+
}
|
|
251
266
|
const declarations =
|
|
252
267
|
selection.kind === "files"
|
|
253
268
|
? yield* readDeclarationsIn(process.cwd(), selection.files)
|
|
@@ -255,6 +270,16 @@ const readScope = Effect.fn("readScope")(function* (selection: SuiteSelection, s
|
|
|
255
270
|
return { files: undefined, declarations };
|
|
256
271
|
});
|
|
257
272
|
|
|
273
|
+
const changedSelection = Effect.fn("changedSelection")(function* () {
|
|
274
|
+
const e2e = yield* changedE2eFiles(process.cwd());
|
|
275
|
+
yield* Console.log(
|
|
276
|
+
e2e.length === 0
|
|
277
|
+
? `${NAME}: the change touches no e2e test files, so only the unit tier runs`
|
|
278
|
+
: `${NAME}: the change touches ${e2e.length} e2e test file(s): ${e2e.join(" ")}`,
|
|
279
|
+
);
|
|
280
|
+
return { kind: "changed", e2e } satisfies SuiteSelection;
|
|
281
|
+
});
|
|
282
|
+
|
|
258
283
|
const runSuite = Effect.fn("runSuite")(function* (outfile: string, args: readonly string[], workers: number | undefined) {
|
|
259
284
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
260
285
|
const command = ["test", "--randomize", `--parallel=${workerCount(workers)}`, ...args, ...reporterArgs(outfile)];
|
|
@@ -272,17 +297,23 @@ const runSuite = Effect.fn("runSuite")(function* (outfile: string, args: readonl
|
|
|
272
297
|
const testEntry = Effect.gen(function* () {
|
|
273
298
|
const entry = entryOptions(process.argv.slice(2));
|
|
274
299
|
if (entry === undefined) return yield* new Usage({ message: USAGE });
|
|
275
|
-
const { options, junit } = entry;
|
|
300
|
+
const { options, junit, changed } = entry;
|
|
276
301
|
const fs = yield* FileSystem.FileSystem;
|
|
277
302
|
const path = yield* Path.Path;
|
|
278
303
|
const environment = yield* environmentOf;
|
|
279
|
-
const selection =
|
|
304
|
+
const selection = changed
|
|
305
|
+
? yield* changedSelection()
|
|
306
|
+
: yield* narrowSelection(options, selectSuite(options, environment), environment);
|
|
280
307
|
if (selection.kind === "files" && selection.files.length === 0) {
|
|
281
308
|
yield* Console.log(`${NAME}: the pull request changes no file a test reads, so no test ran`);
|
|
282
309
|
return true;
|
|
283
310
|
}
|
|
284
311
|
const smoke = selection.kind === "smoke" ? yield* readSmokeList(process.cwd()) : [];
|
|
285
312
|
const scope = yield* readScope(selection, smoke);
|
|
313
|
+
if (scope.files?.length === 0 && selection.kind === "changed") {
|
|
314
|
+
yield* Console.log(`${NAME}: no unit test and no e2e test the change touches, so no test ran`);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
286
317
|
if (scope.files?.length === 0) {
|
|
287
318
|
yield* Console.log(`${NAME}: no unit test and no smoke list, so this local run ran nothing; declare checks.smoke or pass ${FULL}`);
|
|
288
319
|
return true;
|