@avi2dg/checks 0.41.1 → 0.43.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 +16 -0
- package/README.md +5 -1
- package/docs/gates/checks-advisories.md +2 -0
- package/docs/gates/checks-ci-wiring.md +25 -1
- package/docs/gates/checks-flake.md +2 -0
- package/docs/gates/checks-mutation-compare.md +2 -0
- package/docs/gates/checks-release-notes.md +6 -0
- package/docs/gates/checks-test-layout.md +3 -2
- package/docs/gates/checks-test.md +31 -5
- package/package.json +2 -1
- package/src/delivery/bun-pin.ts +174 -0
- package/src/delivery/ci-wiring.ts +8 -7
- package/src/delivery/workflow-references.ts +6 -1
- package/src/testing/flake.ts +1 -1
- package/src/testing/test-layout.ts +13 -4
- package/src/testing/test-report.ts +20 -5
- package/src/testing/test.ts +36 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.43.0
|
|
6
|
+
|
|
7
|
+
Released 2026-10-17.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **testing:** run checks-test files in parallel on opt-in [#158](https://github.com/avi2d/checks/pull/158)
|
|
12
|
+
|
|
13
|
+
## 0.42.0
|
|
14
|
+
|
|
15
|
+
Released 2026-10-14.
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- **delivery:** fail a setup-bun step whose Bun version drifts from the fleet pin [#156](https://github.com/avi2d/checks/pull/156)
|
|
20
|
+
|
|
5
21
|
## 0.41.1
|
|
6
22
|
|
|
7
23
|
Released 2026-10-14.
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Each repository owns its workflows and native tool configs, as [Native settings]
|
|
|
10
10
|
<!-- generated prerequisites: bun run build writes it from package.json, .bun-version and scripts/doc-blocks.ts -->
|
|
11
11
|
|
|
12
12
|
- A git repository, whose history the range gates read.
|
|
13
|
-
- Bun 1.4.
|
|
13
|
+
- Bun 1.4.3, which runs every bin.
|
|
14
14
|
- The peer dependencies, at the exact versions the kit pins:
|
|
15
15
|
- `@effect/tsgo` 0.45.0
|
|
16
16
|
- `@swc/core` 1.16.2
|
|
@@ -96,6 +96,8 @@ To consume the kit from a repository:
|
|
|
96
96
|
"test": "checks-test"
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
1. Write the Bun version [Before you begin](#before-you-begin) names to `.bun-version`, which [checks-ci-wiring](docs/gates/checks-ci-wiring.md#bun-version) holds every setup-bun step to.
|
|
100
|
+
|
|
99
101
|
1. Run the scripts on every pull request in `.github/workflows/ci.yml`, fetching the whole history the range needs:
|
|
100
102
|
|
|
101
103
|
```yaml
|
|
@@ -109,6 +111,8 @@ To consume the kit from a repository:
|
|
|
109
111
|
with:
|
|
110
112
|
fetch-depth: 0
|
|
111
113
|
- uses: oven-sh/setup-bun@v2
|
|
114
|
+
with:
|
|
115
|
+
bun-version-file: .bun-version
|
|
112
116
|
- run: bun install --frozen-lockfile
|
|
113
117
|
- run: bun run build
|
|
114
118
|
- run: git diff --exit-code
|
|
@@ -49,9 +49,33 @@ In a private repository each mutation job carries exactly the labels `[self-host
|
|
|
49
49
|
A public repository may keep `runs-on: ubuntu-latest` on every job, because a pull request from a fork runs its own code on the runner.
|
|
50
50
|
The check knows a repository is private only from GitHub's event, so a run outside CI judges only the rules that hold in either.
|
|
51
51
|
|
|
52
|
+
### Bun version
|
|
53
|
+
|
|
54
|
+
It also judges every step that uses `oven-sh/setup-bun`.
|
|
55
|
+
A step at ref `v2`, `v2.<minor>` or `v2.<minor>.<patch>` resolves its effective version in the order setup-bun v2 reads it.
|
|
56
|
+
A step at any other ref or at none fails, because the check knows only setup-bun v2's version resolution.
|
|
57
|
+
An input name matches in any case, as GitHub reads it.
|
|
58
|
+
The step input `with.bun-version` wins when it names a version, and an unquoted number counts as the text GitHub passes.
|
|
59
|
+
Else the file the step input `with.bun-version-file` names decides by its basename, when it yields a version.
|
|
60
|
+
A `package.json` file reads `packageManager` after `bun@`, and reads `engines.bun` only when `packageManager` is absent or holds no `bun@`.
|
|
61
|
+
A `.tool-versions` file reads the rest of the first line that starts with `bun`.
|
|
62
|
+
A `.bun-version` or `.bumrc` file reads the whole trimmed file.
|
|
63
|
+
Else the `package.json` at the workspace root reads the same way.
|
|
64
|
+
Else setup-bun installs latest.
|
|
65
|
+
A blank version counts as none.
|
|
66
|
+
A repository passes by writing the fleet version to `.bun-version` and naming that file in each step's `with.bun-version-file`.
|
|
67
|
+
The step fails the check when the version is missing or differs from the fleet bun version.
|
|
68
|
+
A range fails because it differs from the fleet version.
|
|
69
|
+
A version built from an expression fails because it cannot be shown equal before the run.
|
|
70
|
+
This rule skips a repository with no setup-bun step.
|
|
71
|
+
The fleet bun version equals the dotfiles mise pin.
|
|
72
|
+
A bump moves both together, so a kit release that moves the fleet version turns each consumer red until its pin follows.
|
|
73
|
+
Each refusal names the workflow file and line, the version found and where it was read, and the fleet version.
|
|
74
|
+
|
|
52
75
|
## What it reads
|
|
53
76
|
|
|
54
77
|
The bin reads `.github/workflows/*.yml`, `*.yaml`, the `scripts` in `package.json` and the shell scripts those steps and scripts run from the working tree.
|
|
78
|
+
It reads each file a setup-bun step names for its version.
|
|
55
79
|
It reads the target branch from `GITHUB_BASE_REF`, then from `refs/remotes/origin/HEAD` and then from the event file `GITHUB_EVENT_PATH` names.
|
|
56
80
|
It reads whether the repository is private from `repository.private` in that event file.
|
|
57
81
|
It parses the workflows with `Bun.YAML` without executing them.
|
|
@@ -65,7 +89,7 @@ It takes no arguments.
|
|
|
65
89
|
| Code | Result |
|
|
66
90
|
| --- | --- |
|
|
67
91
|
| 0 | Every required command has a reachable step. |
|
|
68
|
-
| 1 | A required command is missing or blocked, a job runs on the wrong runner,
|
|
92
|
+
| 1 | A required command is missing or blocked, a job runs on the wrong runner, a workflow names a missing workflow or job, or a setup-bun step drifts from the fleet bun version. |
|
|
69
93
|
| 2 | A workflow or `package.json` cannot be decoded. |
|
|
70
94
|
|
|
71
95
|
## Sample output
|
|
@@ -67,6 +67,8 @@ jobs:
|
|
|
67
67
|
steps:
|
|
68
68
|
- uses: actions/checkout@v5
|
|
69
69
|
- uses: oven-sh/setup-bun@v2
|
|
70
|
+
with:
|
|
71
|
+
bun-version-file: .bun-version
|
|
70
72
|
- run: bun install --frozen-lockfile
|
|
71
73
|
- run: bunx checks-flake --runs 10 --report flake-report.json
|
|
72
74
|
- uses: actions/upload-artifact@v4
|
|
@@ -69,6 +69,8 @@ jobs:
|
|
|
69
69
|
- name: ref is a tag
|
|
70
70
|
run: test "$GITHUB_REF_TYPE" = tag
|
|
71
71
|
- uses: oven-sh/setup-bun@v2
|
|
72
|
+
with:
|
|
73
|
+
bun-version-file: .bun-version
|
|
72
74
|
- run: bun install --frozen-lockfile
|
|
73
75
|
- run: bun run build
|
|
74
76
|
- run: git diff --exit-code
|
|
@@ -89,6 +91,8 @@ jobs:
|
|
|
89
91
|
with:
|
|
90
92
|
fetch-depth: 0
|
|
91
93
|
- uses: oven-sh/setup-bun@v2
|
|
94
|
+
with:
|
|
95
|
+
bun-version-file: .bun-version
|
|
92
96
|
- run: bun install --frozen-lockfile
|
|
93
97
|
- name: extract release notes
|
|
94
98
|
run: ./node_modules/.bin/checks-release-notes "${GITHUB_REF_NAME#v}" "$RUNNER_TEMP/release-notes.md"
|
|
@@ -120,6 +124,8 @@ jobs:
|
|
|
120
124
|
- name: ref is a tag
|
|
121
125
|
run: test "$GITHUB_REF_TYPE" = tag
|
|
122
126
|
- uses: oven-sh/setup-bun@v2
|
|
127
|
+
with:
|
|
128
|
+
bun-version-file: .bun-version
|
|
123
129
|
- run: bun install --frozen-lockfile
|
|
124
130
|
- run: bun run build
|
|
125
131
|
- run: git diff --exit-code
|
|
@@ -20,12 +20,13 @@ It fails unless the repository holds this shape, and names the file and the path
|
|
|
20
20
|
Bun ignores both directories in the default suite.
|
|
21
21
|
A live tier with test files requires `test:live` set to `checks-test --tier=live`.
|
|
22
22
|
A pixel tier with test files requires `test:pixel` set to `checks-test --tier=pixel`.
|
|
23
|
+
Either tier script may add `--parallel=<workers>`.
|
|
23
24
|
- A test runs in-process or in a process of its own.
|
|
24
25
|
A test outside the `e2e`, `live` and `pixel` levels runs in-process, quarantined or not, so it may not import `node:child_process`, `net`, `http`, `https`, `http2`, `tls` or `dgram`.
|
|
25
26
|
It may not import `$`, `spawn`, `spawnSync`, `connect`, `serve` or `listen` from `bun`, may not touch `Bun.$` or `Bun.spawn`, and may not call `fetch`.
|
|
26
27
|
A test at the `e2e`, `live` or `pixel` level may do all of it, under `tests/quarantine/` as well.
|
|
27
28
|
Helpers in `tests/lib/**` answer to the same rule, since an in-process test reaches them.
|
|
28
|
-
- `scripts.test` is
|
|
29
|
+
- `scripts.test` is `checks-test`, optionally with `--parallel=<workers>`, which runs `bun test --randomize` as [checks-test](checks-test.md) says.
|
|
29
30
|
- `scripts.lint` runs this check, itself or through `checks-lint` called by its bare bin name.
|
|
30
31
|
- `bunfig.toml` carries every `[test]` key of the shipped preset with the same value.
|
|
31
32
|
`[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.
|
|
@@ -78,7 +79,7 @@ It checks the directory it runs in, or the directory it is given.
|
|
|
78
79
|
```
|
|
79
80
|
test-layout: 4 violation(s)
|
|
80
81
|
src/a.test.ts: a test file must live at tests/<level>/**/*.test.ts, or tests/quarantine/<level>/**/*.test.ts while quarantined, with unit, e2e, live, or pixel as the level; move it to tests/unit/a.test.ts
|
|
81
|
-
package.json: scripts.test must be
|
|
82
|
+
package.json: scripts.test must be "checks-test", optionally with --parallel=<workers>, which runs bun test --randomize and judges its skips, found "bun test"
|
|
82
83
|
package.json: scripts.lint must run the layout check: add "checks-lint"
|
|
83
84
|
bunfig.toml: bunfig.toml is missing; bun has no bunfig extends, so copy node_modules/@avi2dg/checks/bunfig.toml
|
|
84
85
|
```
|
|
@@ -47,7 +47,7 @@ A skipped test or a todo without `skipReason` fails in every environment.
|
|
|
47
47
|
It does not read a skip list from `package.json`.
|
|
48
48
|
It counts a run as `ci` when `CI` is true and as `local` otherwise.
|
|
49
49
|
|
|
50
|
-
The command takes no arguments for the default suite.
|
|
50
|
+
The command takes no arguments for the default suite, except the worker count of [Parallel runs](#parallel-runs).
|
|
51
51
|
Use `checks-test --tier=live` or `checks-test --tier=pixel` for a named test tier.
|
|
52
52
|
A tier run clears Bun's ignored paths, runs only `./tests/live` or `./tests/pixel`, and still checks each skip.
|
|
53
53
|
|
|
@@ -72,10 +72,36 @@ Add the matching package scripts when either directory contains tests:
|
|
|
72
72
|
Bun ignores both directories during the default run.
|
|
73
73
|
`checks-test-layout` requires each tier script when its directory contains a test file.
|
|
74
74
|
|
|
75
|
+
## Parallel runs
|
|
76
|
+
|
|
77
|
+
A repository runs its test files in parallel by passing a worker count in its `test` script:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"scripts": {
|
|
82
|
+
"test": "checks-test --parallel=4"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`checks-test --parallel=<workers>` runs `bun test --randomize --parallel=<workers>`, so Bun hands each test file to one of that many worker processes.
|
|
88
|
+
Each file then runs in a fresh global, so a file cannot lean on state another file left behind.
|
|
89
|
+
The count is a whole number from 1 up, and without the option the suite runs serially in one process.
|
|
90
|
+
Tests still run in random order, and `test.only` still fails the run.
|
|
91
|
+
Bun merges every worker's results into the one JUnit report, so each skipped test and each todo is judged as in a serial run.
|
|
92
|
+
A tier script takes the same option, as in `checks-test --tier=live --parallel=4`.
|
|
93
|
+
|
|
94
|
+
A file whose worker crashes, for example when a test calls `process.exit`, fails the run.
|
|
95
|
+
Bun reports no result from that file, so its skips are not judged and its declarations are not reported as stale.
|
|
96
|
+
|
|
97
|
+
`checks-test` takes no timings file, because Bun ignores the order a timings file sets whenever `--randomize` is on.
|
|
98
|
+
A timings option waits for Bun to honour timings under random order.
|
|
99
|
+
|
|
75
100
|
## Arguments
|
|
76
101
|
|
|
77
|
-
The default command takes no arguments.
|
|
78
|
-
Its named tier options are `--tier=live` and `--tier=pixel
|
|
102
|
+
The default command takes no arguments except `--parallel=<workers>`, as [Parallel runs](#parallel-runs) says.
|
|
103
|
+
Its named tier options are `--tier=live` and `--tier=pixel`, and a tier run takes `--parallel=<workers>` too.
|
|
104
|
+
Each option appears at most once.
|
|
79
105
|
It refuses test filters because every test excluded by a filter would look skipped.
|
|
80
106
|
|
|
81
107
|
## Exit codes
|
|
@@ -83,8 +109,8 @@ It refuses test filters because every test excluded by a filter would look skipp
|
|
|
83
109
|
| Code | When |
|
|
84
110
|
| --- | --- |
|
|
85
111
|
| 0 | every test passed and every skip has a matching site reason |
|
|
86
|
-
| 1 | a test failed, a skip lacks a site reason or a declaration is stale in CI |
|
|
87
|
-
| 2 | a declaration cannot be read, `CI` is not a boolean or Bun wrote no report |
|
|
112
|
+
| 1 | a test failed, a parallel worker crashed, a skip lacks a site reason or a declaration is stale in CI |
|
|
113
|
+
| 2 | an argument is not one it takes, a declaration cannot be read, `CI` is not a boolean or Bun wrote no report |
|
|
88
114
|
|
|
89
115
|
## Sample output
|
|
90
116
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"src/delivery/ci-wiring.ts",
|
|
53
53
|
"src/delivery/shell-command.ts",
|
|
54
54
|
"src/delivery/workflow-references.ts",
|
|
55
|
+
"src/delivery/bun-pin.ts",
|
|
55
56
|
"src/delivery/mutation-job.ts",
|
|
56
57
|
"src/delivery/secrets.ts",
|
|
57
58
|
"src/delivery/gitleaks.ts",
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
2
|
+
import { within } from "./mutation-job.ts";
|
|
3
|
+
import { isRecord, locate, workflowJobs, type Workflow } from "./workflow-references.ts";
|
|
4
|
+
|
|
5
|
+
export const FLEET_BUN_VERSION = "1.4.3";
|
|
6
|
+
const ROOT_MANIFEST = "package.json";
|
|
7
|
+
|
|
8
|
+
export type BunPinPolicy = {
|
|
9
|
+
readonly files: ReadonlyMap<string, string>;
|
|
10
|
+
readonly manifests: ReadonlyMap<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type BunPinFault = {
|
|
14
|
+
readonly location: string;
|
|
15
|
+
readonly fault: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SETUP_BUN = /^oven-sh\/setup-bun(?:@(?<ref>.*))?$/i;
|
|
19
|
+
const V2_REF = /^v2(?:\.\d+){0,2}$/;
|
|
20
|
+
|
|
21
|
+
function setupBunRef(uses: unknown): string | undefined {
|
|
22
|
+
const match = typeof uses === "string" ? SETUP_BUN.exec(uses.trim()) : null;
|
|
23
|
+
return match === null ? undefined : (match.groups?.["ref"] ?? "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isExpression(value: string): boolean {
|
|
27
|
+
return value.includes("${{");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function baseName(name: string): string {
|
|
31
|
+
return name.slice(name.lastIndexOf("/") + 1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function actionInput(step: Readonly<Record<string, unknown>>, name: string): string {
|
|
35
|
+
const inputs = isRecord(step["with"]) ? step["with"] : {};
|
|
36
|
+
const value = Object.entries(inputs).find(([key]) => key.toLowerCase() === name)?.[1];
|
|
37
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value).trim() : "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nonEmpty(version: string | undefined): string | undefined {
|
|
41
|
+
const trimmed = version?.trim();
|
|
42
|
+
return trimmed === "" ? undefined : trimmed;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function manifestBunVersion(manifest: unknown): string | undefined {
|
|
46
|
+
if (!isRecord(manifest)) return undefined;
|
|
47
|
+
const manager = manifest["packageManager"];
|
|
48
|
+
const engines = manifest["engines"];
|
|
49
|
+
const engine = isRecord(engines) ? engines["bun"] : undefined;
|
|
50
|
+
const version = manager === undefined || manager === null ? engine : typeof manager === "string" ? (manager.split("bun@")[1] ?? engine) : undefined;
|
|
51
|
+
return typeof version === "string" ? nonEmpty(version) : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const FILE_READERS = new Map<string, (text: string) => string | undefined>([
|
|
55
|
+
[".tool-versions", (text) => /^bun\s*(?<version>.*?)$/m.exec(text)?.groups?.["version"]],
|
|
56
|
+
[".bumrc", (text) => text],
|
|
57
|
+
[".bun-version", (text) => text],
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
function fileBunVersion(policy: BunPinPolicy, key: string): string | undefined {
|
|
61
|
+
if (baseName(key) === "package.json") return manifestBunVersion(policy.manifests.get(key));
|
|
62
|
+
const text = policy.files.get(key);
|
|
63
|
+
const read = FILE_READERS.get(baseName(key));
|
|
64
|
+
return text === undefined || read === undefined ? undefined : nonEmpty(read(text));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type BunPinRequest = {
|
|
68
|
+
readonly version: string | undefined;
|
|
69
|
+
readonly source: string;
|
|
70
|
+
readonly expression: string | undefined;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function rootRequest(policy: BunPinPolicy, consulted: string): BunPinRequest {
|
|
74
|
+
const version = manifestBunVersion(policy.manifests.get(ROOT_MANIFEST));
|
|
75
|
+
return { version, source: version === undefined ? consulted : ROOT_MANIFEST, expression: undefined };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function bunPinRequest(policy: BunPinPolicy, step: Readonly<Record<string, unknown>>): BunPinRequest {
|
|
79
|
+
const pinned = actionInput(step, "bun-version");
|
|
80
|
+
if (pinned !== "") {
|
|
81
|
+
return isExpression(pinned)
|
|
82
|
+
? { version: undefined, source: "with.bun-version", expression: pinned }
|
|
83
|
+
: { version: pinned, source: "with.bun-version", expression: undefined };
|
|
84
|
+
}
|
|
85
|
+
const file = actionInput(step, "bun-version-file");
|
|
86
|
+
if (file === "") return rootRequest(policy, ROOT_MANIFEST);
|
|
87
|
+
const source = `with.bun-version-file ${file}`;
|
|
88
|
+
if (isExpression(file)) return { version: undefined, source, expression: file };
|
|
89
|
+
const key = within("", file);
|
|
90
|
+
const version = key === undefined ? undefined : fileBunVersion(policy, key);
|
|
91
|
+
return version === undefined ? rootRequest(policy, `${source} or ${ROOT_MANIFEST}`) : { version, source, expression: undefined };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function bunPinFault(policy: BunPinPolicy, step: Readonly<Record<string, unknown>>): string | undefined {
|
|
95
|
+
const request = bunPinRequest(policy, step);
|
|
96
|
+
if (request.expression !== undefined) {
|
|
97
|
+
return `requests bun ${request.expression} from ${request.source}, which cannot be shown equal to ${FLEET_BUN_VERSION} before the run`;
|
|
98
|
+
}
|
|
99
|
+
if (request.version === undefined) {
|
|
100
|
+
return `pins no bun version from ${request.source}, so setup-bun installs latest, want ${FLEET_BUN_VERSION}`;
|
|
101
|
+
}
|
|
102
|
+
return request.version === FLEET_BUN_VERSION ? undefined : `requests bun ${request.version} from ${request.source}, want ${FLEET_BUN_VERSION}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function unknownRefFault(ref: string): string {
|
|
106
|
+
return `uses oven-sh/setup-bun at ${ref === "" ? "no ref" : ref}, but the check knows only setup-bun v2's version resolution, so the step must use oven-sh/setup-bun@v2`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function findBunPinFaults(policy: BunPinPolicy, workflows: readonly Workflow[]): readonly BunPinFault[] {
|
|
110
|
+
return workflows.flatMap((workflow) => {
|
|
111
|
+
const at = locate(workflow.path, workflow.text.split("\n"));
|
|
112
|
+
return workflowJobs(workflow).flatMap(([id, job]) => {
|
|
113
|
+
const steps = Array.isArray(job["steps"]) ? job["steps"] : [];
|
|
114
|
+
return steps.flatMap((step: unknown, index: number) => {
|
|
115
|
+
if (!isRecord(step)) return [];
|
|
116
|
+
const ref = setupBunRef(step["uses"]);
|
|
117
|
+
if (ref === undefined) return [];
|
|
118
|
+
const fault = V2_REF.test(ref) ? bunPinFault(policy, step) : unknownRefFault(ref);
|
|
119
|
+
return fault === undefined ? [] : [{ location: at(["jobs", id, "steps", index, "uses"], String(step["uses"]), `job ${id} step ${index + 1}`), fault }];
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function formatBunPinReport(faults: readonly BunPinFault[]): string {
|
|
126
|
+
return [`ci-wiring: ${faults.length} setup-bun step(s) drift from the fleet bun version:`, ...faults.map(({ location, fault }) => ` ${location}: ${fault}`)].join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function versionFileKeys(job: Readonly<Record<string, unknown>>): readonly string[] {
|
|
130
|
+
const steps = Array.isArray(job["steps"]) ? job["steps"] : [];
|
|
131
|
+
const keys: string[] = [];
|
|
132
|
+
for (const step of steps) {
|
|
133
|
+
if (!isRecord(step) || !V2_REF.test(setupBunRef(step["uses"]) ?? "")) continue;
|
|
134
|
+
const file = actionInput(step, "bun-version-file");
|
|
135
|
+
if (file === "" || isExpression(file)) continue;
|
|
136
|
+
const key = within("", file);
|
|
137
|
+
if (key !== undefined) keys.push(key);
|
|
138
|
+
}
|
|
139
|
+
return keys;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function wantedBunPinFiles(workflows: readonly Workflow[]): ReadonlySet<string> {
|
|
143
|
+
const wanted = new Set<string>([ROOT_MANIFEST]);
|
|
144
|
+
for (const workflow of workflows) {
|
|
145
|
+
for (const [, job] of workflowJobs(workflow)) {
|
|
146
|
+
for (const key of versionFileKeys(job)) wanted.add(key);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return wanted;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const readJsonFile = Effect.fn("readJsonFile")(function* (file: string) {
|
|
153
|
+
return yield* (yield* FileSystem.FileSystem).readFileString(file).pipe(
|
|
154
|
+
Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown))),
|
|
155
|
+
Effect.orElseSucceed((): undefined => undefined),
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
export const readBunPinPolicy = Effect.fn("readBunPinPolicy")(function* (root: string, workflows: readonly Workflow[]) {
|
|
160
|
+
const path = yield* Path.Path;
|
|
161
|
+
const fs = yield* FileSystem.FileSystem;
|
|
162
|
+
const files = new Map<string, string>();
|
|
163
|
+
const manifests = new Map<string, unknown>();
|
|
164
|
+
for (const name of wantedBunPinFiles(workflows)) {
|
|
165
|
+
if (baseName(name) === "package.json") {
|
|
166
|
+
const parsed: unknown = yield* readJsonFile(path.join(root, name));
|
|
167
|
+
if (parsed !== undefined) manifests.set(name, parsed);
|
|
168
|
+
} else {
|
|
169
|
+
const text = yield* fs.readFileString(path.join(root, name)).pipe(Effect.orElseSucceed((): undefined => undefined));
|
|
170
|
+
if (text !== undefined) files.set(name, text);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { files, manifests };
|
|
174
|
+
});
|
|
@@ -3,9 +3,13 @@ import { Config, Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
|
3
3
|
import { defaultBranch, git } from "../core/git.ts";
|
|
4
4
|
import { runMain } from "../core/main.ts";
|
|
5
5
|
import { ENTRY_POINT, KIT_GATES, type KitGate } from "../core/gates.ts";
|
|
6
|
+
import { findBunPinFaults, formatBunPinReport, readBunPinPolicy } from "./bun-pin.ts";
|
|
6
7
|
import { readShellFiles, runsMutation, within, type Run } from "./mutation-job.ts";
|
|
7
8
|
import { invokes, mentions, plainCommand, type Command } from "./shell-command.ts";
|
|
8
|
-
import { findMissingWorkflows, formatMissingReport, isRecord, names, type Workflow } from "./workflow-references.ts";
|
|
9
|
+
import { findMissingWorkflows, formatMissingReport, isRecord, names, workflowJobs, type Workflow } from "./workflow-references.ts";
|
|
10
|
+
|
|
11
|
+
export { FLEET_BUN_VERSION, findBunPinFaults, formatBunPinReport } from "./bun-pin.ts";
|
|
12
|
+
export type { BunPinFault, BunPinPolicy } from "./bun-pin.ts";
|
|
9
13
|
|
|
10
14
|
export type { Command };
|
|
11
15
|
|
|
@@ -297,11 +301,6 @@ function jobRuns(workflow: Workflow, job: Readonly<Record<string, unknown>>): re
|
|
|
297
301
|
return steps.flatMap((step: unknown) => (isRecord(step) && typeof step["run"] === "string" ? [{ script: step["run"], dir: stepDirectory(step["working-directory"], dir) }] : []));
|
|
298
302
|
}
|
|
299
303
|
|
|
300
|
-
function workflowJobs(workflow: Workflow): readonly (readonly [string, Readonly<Record<string, unknown>>])[] {
|
|
301
|
-
const jobs = isRecord(workflow.document) ? workflow.document["jobs"] : undefined;
|
|
302
|
-
return isRecord(jobs) ? Object.entries(jobs).flatMap(([id, job]) => (isRecord(job) ? [[id, job] as const] : [])) : [];
|
|
303
|
-
}
|
|
304
|
-
|
|
305
304
|
export function findRunnerFaults(policy: RunnerPolicy, workflows: readonly Workflow[]): readonly RunnerFault[] {
|
|
306
305
|
return workflows.flatMap((workflow) =>
|
|
307
306
|
workflowJobs(workflow).flatMap(([id, job]) => {
|
|
@@ -378,7 +377,9 @@ const wiring = Effect.gen(function* () {
|
|
|
378
377
|
const runs = workflows.flatMap((workflow) => workflowJobs(workflow).flatMap(([, job]) => jobRuns(workflow, job)));
|
|
379
378
|
const faults = findRunnerFaults({ visibility: yield* readVisibility, scripts, files: yield* readShellFiles(root, scripts, runs) }, workflows);
|
|
380
379
|
if (faults.length > 0) yield* Console.error(formatRunnerReport(faults));
|
|
381
|
-
|
|
380
|
+
const bunFaults = findBunPinFaults(yield* readBunPinPolicy(root, workflows), workflows);
|
|
381
|
+
if (bunFaults.length > 0) yield* Console.error(formatBunPinReport(bunFaults));
|
|
382
|
+
return gaps.length === 0 && faults.length === 0 && missing.length === 0 && bunFaults.length === 0;
|
|
382
383
|
});
|
|
383
384
|
|
|
384
385
|
if (import.meta.main) runMain("ci-wiring", wiring);
|
|
@@ -15,6 +15,11 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
15
15
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export function workflowJobs(workflow: Workflow): readonly (readonly [string, Readonly<Record<string, unknown>>])[] {
|
|
19
|
+
const jobs = isRecord(workflow.document) ? workflow.document["jobs"] : undefined;
|
|
20
|
+
return isRecord(jobs) ? Object.entries(jobs).flatMap(([id, job]) => (isRecord(job) ? [[id, job] as const] : [])) : [];
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
export function names(value: unknown): readonly string[] | undefined {
|
|
19
24
|
if (typeof value === "string") return [value];
|
|
20
25
|
if (!Array.isArray(value)) return undefined;
|
|
@@ -108,7 +113,7 @@ function nodeAt(lines: readonly string[], keys: readonly Key[]): Node | undefine
|
|
|
108
113
|
);
|
|
109
114
|
}
|
|
110
115
|
|
|
111
|
-
function locate(path: string, lines: readonly string[]): Locator {
|
|
116
|
+
export function locate(path: string, lines: readonly string[]): Locator {
|
|
112
117
|
const used = new Map<string, number>();
|
|
113
118
|
return (keys, needle, fallback) => {
|
|
114
119
|
const node = nodeAt(lines, keys);
|
package/src/testing/flake.ts
CHANGED
|
@@ -75,7 +75,7 @@ const runOnce = Effect.fn("runOnce")(function* (seed: number, outfile: string) {
|
|
|
75
75
|
if (passed) return yield* new ReportError({ message: `bun test passed with seed ${seed} but wrote no report` });
|
|
76
76
|
return { seed, passed, tests: 0, failed: [] } satisfies Run;
|
|
77
77
|
}
|
|
78
|
-
const results = yield* parseReport(yield* fs.readFileString(outfile));
|
|
78
|
+
const { results } = yield* parseReport(yield* fs.readFileString(outfile));
|
|
79
79
|
return { seed, passed, tests: results.length, failed: results.filter((result) => result.outcome === "failed") } satisfies Run;
|
|
80
80
|
});
|
|
81
81
|
|
|
@@ -4,6 +4,7 @@ import { git } from "../core/git.ts";
|
|
|
4
4
|
import { runMain } from "../core/main.ts";
|
|
5
5
|
import { ENTRY_POINT, TEST_ENTRY_POINT } from "../core/gates.ts";
|
|
6
6
|
import { identifierName, isRecord, lineOf, parseTypeScript, spanStart, stringValue } from "../core/swc.ts";
|
|
7
|
+
import { testOptions, type TestOptions } from "./test.ts";
|
|
7
8
|
|
|
8
9
|
export type Violation = {
|
|
9
10
|
readonly file: string;
|
|
@@ -61,6 +62,7 @@ export const LAYOUT_CHECK_MARK = "src/testing/test-layout.ts";
|
|
|
61
62
|
export const LAYOUT_CHECK_BIN = "checks-test-layout";
|
|
62
63
|
const OWN_ENTRY_POINT = `src/${ENTRY_POINT.script}`;
|
|
63
64
|
const TEST_SCRIPTS: readonly string[] = [TEST_ENTRY_POINT.bin, `bun src/${TEST_ENTRY_POINT.script}`];
|
|
65
|
+
const PARALLEL_OPTION = "optionally with --parallel=<workers>";
|
|
64
66
|
|
|
65
67
|
export class LayoutError extends Schema.TaggedError<LayoutError>()("LayoutError", {
|
|
66
68
|
message: Schema.String,
|
|
@@ -229,6 +231,12 @@ export const isolationViolations = Effect.fn("isolationViolations")(function* (f
|
|
|
229
231
|
return violations;
|
|
230
232
|
});
|
|
231
233
|
|
|
234
|
+
function entryOptions(script: unknown): TestOptions | undefined {
|
|
235
|
+
if (typeof script !== "string") return undefined;
|
|
236
|
+
const entry = TEST_SCRIPTS.find((candidate) => script === candidate || script.startsWith(`${candidate} `));
|
|
237
|
+
return entry === undefined ? undefined : testOptions(script.slice(entry.length).split(" ").filter((arg) => arg !== ""));
|
|
238
|
+
}
|
|
239
|
+
|
|
232
240
|
function tierScriptViolation(scripts: unknown, files: readonly string[], tier: (typeof TEST_TIERS)[number]): Violation | undefined {
|
|
233
241
|
const file = "package.json";
|
|
234
242
|
const key = `test:${tier}`;
|
|
@@ -236,11 +244,11 @@ function tierScriptViolation(scripts: unknown, files: readonly string[], tier: (
|
|
|
236
244
|
const expected = `${TEST_ENTRY_POINT.bin} --tier=${tier}`;
|
|
237
245
|
const hasTests = files.some((candidate) => candidate.startsWith(`tests/${tier}/`) && TEST_FILE.test(candidate));
|
|
238
246
|
if (!hasTests && found === undefined) return undefined;
|
|
239
|
-
if (found ===
|
|
247
|
+
if (entryOptions(found)?.tier === tier) return undefined;
|
|
240
248
|
return {
|
|
241
249
|
file,
|
|
242
250
|
line: undefined,
|
|
243
|
-
message: `${key} must be "${expected}"${hasTests ? " when its test tier has files" : " when declared"}, found ${JSON.stringify(found ?? null)}`,
|
|
251
|
+
message: `${key} must be "${expected}", ${PARALLEL_OPTION},${hasTests ? " when its test tier has files" : " when declared"}, found ${JSON.stringify(found ?? null)}`,
|
|
244
252
|
};
|
|
245
253
|
}
|
|
246
254
|
|
|
@@ -250,11 +258,12 @@ export function scriptViolations(manifest: unknown, files: readonly string[] = [
|
|
|
250
258
|
const test = isRecord(scripts) ? scripts["test"] : undefined;
|
|
251
259
|
const lint = isRecord(scripts) ? scripts["lint"] : undefined;
|
|
252
260
|
const violations: Violation[] = [];
|
|
253
|
-
|
|
261
|
+
const options = entryOptions(test);
|
|
262
|
+
if (options === undefined || options.tier !== undefined) {
|
|
254
263
|
violations.push({
|
|
255
264
|
file,
|
|
256
265
|
line: undefined,
|
|
257
|
-
message: `scripts.test must be
|
|
266
|
+
message: `scripts.test must be "${TEST_ENTRY_POINT.bin}", ${PARALLEL_OPTION}, which runs bun test --randomize and judges its skips, found ${JSON.stringify(test ?? null)}`,
|
|
258
267
|
});
|
|
259
268
|
}
|
|
260
269
|
if (typeof lint !== "string" || !runsLayoutCheck(lint)) {
|
|
@@ -9,6 +9,11 @@ export type TestResult = {
|
|
|
9
9
|
readonly outcome: Outcome;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
type Report = {
|
|
13
|
+
readonly results: readonly TestResult[];
|
|
14
|
+
readonly crashedFiles: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
12
17
|
export class ReportError extends Schema.TaggedError<ReportError>()("ReportError", {
|
|
13
18
|
message: Schema.String,
|
|
14
19
|
}) {}
|
|
@@ -36,6 +41,7 @@ const decodeSuite = Schema.decodeUnknownEffect(Schema.Struct({ name: Schema.Stri
|
|
|
36
41
|
const decodeCase = Schema.decodeUnknownEffect(
|
|
37
42
|
Schema.Struct({ name: Schema.String, file: Schema.String, line: Schema.NumberFromString.check(Schema.isInt()) }),
|
|
38
43
|
);
|
|
44
|
+
const decodeCrash = Schema.decodeUnknownEffect(Schema.Struct({ file: Schema.String }));
|
|
39
45
|
|
|
40
46
|
function unescape(text: string): string {
|
|
41
47
|
return text.replace(ENTITY, (_, named: string | undefined, decimal: string | undefined, hex: string | undefined) => {
|
|
@@ -91,20 +97,28 @@ function outcomeOf(testcase: Element): Outcome {
|
|
|
91
97
|
return testcase.children.some((child) => child.tag === "failure" || child.tag === "error") ? "failed" : "passed";
|
|
92
98
|
}
|
|
93
99
|
|
|
100
|
+
// bun's parallel coordinator replaces every result of a file whose worker crashed with one failure that has no line.
|
|
101
|
+
function crashed(testcase: Element): boolean {
|
|
102
|
+
return testcase.attributes["line"] === undefined && outcomeOf(testcase) === "failed";
|
|
103
|
+
}
|
|
104
|
+
|
|
94
105
|
const collect = Effect.fnUntraced(function* (
|
|
95
106
|
suite: Element,
|
|
96
107
|
describes: readonly string[],
|
|
97
|
-
results: TestResult[],
|
|
108
|
+
report: { readonly results: TestResult[]; readonly crashedFiles: string[] },
|
|
98
109
|
): Effect.fn.Return<void, ReportError> {
|
|
99
110
|
const unreadable = (tag: string) => (cause: { readonly message: string }) =>
|
|
100
111
|
new ReportError({ message: `the junit report holds a <${tag}> bun never writes: ${cause.message}` });
|
|
101
112
|
for (const child of suite.children) {
|
|
102
113
|
if (child.tag === "testsuite") {
|
|
103
114
|
const { name } = yield* decodeSuite(child.attributes).pipe(Effect.mapError(unreadable(child.tag)));
|
|
104
|
-
yield* collect(child, [...describes, name],
|
|
115
|
+
yield* collect(child, [...describes, name], report);
|
|
116
|
+
} else if (child.tag === "testcase" && crashed(child)) {
|
|
117
|
+
const { file } = yield* decodeCrash(child.attributes).pipe(Effect.mapError(unreadable(child.tag)));
|
|
118
|
+
report.crashedFiles.push(file);
|
|
105
119
|
} else if (child.tag === "testcase") {
|
|
106
120
|
const { name, file, line } = yield* decodeCase(child.attributes).pipe(Effect.mapError(unreadable(child.tag)));
|
|
107
|
-
results.push({ file, name: [...describes, name].join(NAME_SEPARATOR), line, outcome: outcomeOf(child) });
|
|
121
|
+
report.results.push({ file, name: [...describes, name].join(NAME_SEPARATOR), line, outcome: outcomeOf(child) });
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
});
|
|
@@ -113,8 +127,9 @@ const collect = Effect.fnUntraced(function* (
|
|
|
113
127
|
export const parseReport = Effect.fn("parseReport")(function* (xml: string) {
|
|
114
128
|
const root = yield* parseElements(xml);
|
|
115
129
|
const results: TestResult[] = [];
|
|
130
|
+
const crashedFiles: string[] = [];
|
|
116
131
|
for (const fileSuite of root.children) {
|
|
117
|
-
if (fileSuite.tag === "testsuite") yield* collect(fileSuite, [], results);
|
|
132
|
+
if (fileSuite.tag === "testsuite") yield* collect(fileSuite, [], { results, crashedFiles });
|
|
118
133
|
}
|
|
119
|
-
return results;
|
|
134
|
+
return { results, crashedFiles } satisfies Report;
|
|
120
135
|
});
|
package/src/testing/test.ts
CHANGED
|
@@ -20,12 +20,35 @@ export class SkipGateError extends Schema.TaggedError<SkipGateError>()("SkipGate
|
|
|
20
20
|
message: Schema.String,
|
|
21
21
|
}) {}
|
|
22
22
|
|
|
23
|
+
export type TestOptions = {
|
|
24
|
+
readonly tier?: TestTier;
|
|
25
|
+
readonly workers?: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
23
28
|
const NAME = TEST_ENTRY_POINT.bin;
|
|
24
|
-
const USAGE = `usage: ${NAME}
|
|
29
|
+
const USAGE = `usage: ${NAME} [--tier=live | --tier=pixel] [--parallel=<workers>], and it refuses test filters`;
|
|
25
30
|
const TIERS = new Map<string, TestTier>([
|
|
26
31
|
["--tier=live", "live"],
|
|
27
32
|
["--tier=pixel", "pixel"],
|
|
28
33
|
]);
|
|
34
|
+
const PARALLEL = /^--parallel=([1-9][0-9]*)$/;
|
|
35
|
+
|
|
36
|
+
function optionOf(arg: string): TestOptions | undefined {
|
|
37
|
+
const tier = TIERS.get(arg);
|
|
38
|
+
if (tier !== undefined) return { tier };
|
|
39
|
+
const workers = PARALLEL.exec(arg)?.[1];
|
|
40
|
+
return workers === undefined ? undefined : { workers: Number(workers) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function testOptions(args: readonly string[]): TestOptions | undefined {
|
|
44
|
+
let options: TestOptions = {};
|
|
45
|
+
for (const arg of args) {
|
|
46
|
+
const option = optionOf(arg);
|
|
47
|
+
if (option === undefined || Object.keys(option).some((key) => key in options)) return undefined;
|
|
48
|
+
options = { ...options, ...option };
|
|
49
|
+
}
|
|
50
|
+
return options;
|
|
51
|
+
}
|
|
29
52
|
|
|
30
53
|
function matches(declaration: SkipDeclaration, result: TestResult): boolean {
|
|
31
54
|
const { name } = declaration;
|
|
@@ -113,10 +136,11 @@ const environmentOf = Config.Boolean("CI").pipe(
|
|
|
113
136
|
Effect.mapError((cause) => new SkipGateError({ message: `cannot tell a ci run from a local one: ${cause.message}` })),
|
|
114
137
|
);
|
|
115
138
|
|
|
116
|
-
const runSuite = Effect.fn("runSuite")(function* (outfile: string, tier
|
|
139
|
+
const runSuite = Effect.fn("runSuite")(function* (outfile: string, { tier, workers }: TestOptions) {
|
|
117
140
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
118
141
|
const tierArgs = tier === undefined ? [] : ["--path-ignore-patterns", "", `./tests/${tier}`];
|
|
119
|
-
const
|
|
142
|
+
const parallelArgs = workers === undefined ? [] : [`--parallel=${workers}`];
|
|
143
|
+
const args = ["test", "--randomize", ...parallelArgs, ...tierArgs, ...reporterArgs(outfile)];
|
|
120
144
|
return yield* spawner.exitCode(
|
|
121
145
|
ChildProcess.make(process.execPath, args, {
|
|
122
146
|
stdin: "ignore",
|
|
@@ -129,15 +153,14 @@ const runSuite = Effect.fn("runSuite")(function* (outfile: string, tier: TestTie
|
|
|
129
153
|
});
|
|
130
154
|
|
|
131
155
|
const testEntry = Effect.gen(function* () {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
if (args.length > 0 && tier === undefined) return yield* new Usage({ message: USAGE });
|
|
156
|
+
const options = testOptions(process.argv.slice(2));
|
|
157
|
+
if (options === undefined) return yield* new Usage({ message: USAGE });
|
|
135
158
|
const fs = yield* FileSystem.FileSystem;
|
|
136
|
-
const declarations = yield* readSkipDeclarations(process.cwd(), tier);
|
|
159
|
+
const declarations = yield* readSkipDeclarations(process.cwd(), options.tier);
|
|
137
160
|
const environment = yield* environmentOf;
|
|
138
161
|
|
|
139
162
|
const outfile = (yield* Path.Path).join(yield* fs.makeTempDirectoryScoped({ prefix: "checks-test-" }), "junit.xml");
|
|
140
|
-
const suitePassed = (yield* runSuite(outfile,
|
|
163
|
+
const suitePassed = (yield* runSuite(outfile, options)) === ChildProcessSpawner.ExitCode(0);
|
|
141
164
|
if (!(yield* fs.exists(outfile))) {
|
|
142
165
|
if (!suitePassed) {
|
|
143
166
|
yield* Console.error(`${NAME}: bun test failed before it wrote a report, so no skip was judged`);
|
|
@@ -146,9 +169,12 @@ const testEntry = Effect.gen(function* () {
|
|
|
146
169
|
return yield* new ReportError({ message: `bun test passed but wrote no report to ${outfile}` });
|
|
147
170
|
}
|
|
148
171
|
|
|
149
|
-
const
|
|
172
|
+
const { results, crashedFiles } = yield* parseReport(yield* fs.readFileString(outfile));
|
|
173
|
+
for (const file of crashedFiles) yield* Console.error(`${NAME}: the worker running ${file} crashed, so its skips were not judged`);
|
|
174
|
+
const judged = declarations.filter((declaration) => !crashedFiles.includes(declaration.file));
|
|
175
|
+
const verdict = judgeSkips(results, judged, environment);
|
|
150
176
|
yield* passes(verdict) ? Console.log(report(verdict)) : Console.error(report(verdict));
|
|
151
|
-
return suitePassed && passes(verdict);
|
|
177
|
+
return suitePassed && crashedFiles.length === 0 && passes(verdict);
|
|
152
178
|
}).pipe(Effect.scoped);
|
|
153
179
|
|
|
154
180
|
if (import.meta.main) runMain(NAME, testEntry);
|