apify-test-tools 0.10.2-beta.2 → 0.10.2-beta.21
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/.gitattributes +1 -0
- package/.github/actionlint.yaml +11 -0
- package/.github/actions/checkout-restore-dependencies/action.yaml +58 -6
- package/.github/cli/apify-test-tools.mjs +135 -142
- package/.github/scripts/mask-config-secrets.sh +17 -0
- package/.github/scripts/run-with-apify-tokens.mjs +63 -103
- package/.github/workflows/_check_code.yaml +5 -21
- package/.github/workflows/_move_major_tag.yaml +2 -2
- package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +89 -8
- package/.github/workflows/public_platform-tests.yaml +68 -30
- package/.github/workflows/public_pr-build-test.yaml +77 -38
- package/.github/workflows/public_push-build-latest.yaml +49 -11
- package/.github/workflows/public_review.yaml +5 -4
- package/.github/workflows/public_stale-issues.yaml +51 -0
- package/.husky/pre-commit +4 -4
- package/.prettierignore +2 -1
- package/CHANGELOG.md +23 -0
- package/CONTRIBUTING.md +117 -17
- package/README.md +221 -595
- package/bin/actor-ids.ts +41 -0
- package/bin/affected-actors.ts +211 -0
- package/bin/build-from-local.ts +19 -1
- package/bin/build.ts +105 -39
- package/bin/diff-changes.ts +69 -36
- package/bin/diff-json-schema.ts +5 -6
- package/bin/git.ts +73 -175
- package/bin/main.ts +142 -189
- package/bin/notify.ts +198 -0
- package/bin/run-tests.ts +182 -0
- package/bin/slack.ts +16 -76
- package/bin/stdin-input.ts +156 -0
- package/bin/test-files-glob.ts +126 -0
- package/bin/test-report.ts +141 -140
- package/bin/types.ts +4 -7
- package/bin/utils/config/load-config.ts +20 -2
- package/bin/utils/config/parser.ts +40 -8
- package/bin/utils/config/structures/grouped.ts +2 -2
- package/bin/utils/config/structures/legacy.ts +1 -1
- package/dist/bin/actor-ids.d.ts +15 -0
- package/dist/bin/actor-ids.d.ts.map +1 -0
- package/dist/bin/actor-ids.js +30 -0
- package/dist/bin/actor-ids.js.map +1 -0
- package/dist/bin/affected-actors.d.ts +55 -0
- package/dist/bin/affected-actors.d.ts.map +1 -0
- package/dist/bin/affected-actors.js +142 -0
- package/dist/bin/affected-actors.js.map +1 -0
- package/dist/bin/build-from-local.d.ts +2 -1
- package/dist/bin/build-from-local.d.ts.map +1 -1
- package/dist/bin/build-from-local.js +6 -2
- package/dist/bin/build-from-local.js.map +1 -1
- package/dist/bin/build.d.ts +18 -5
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +74 -24
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +21 -5
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +41 -28
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/diff-json-schema.d.ts +2 -2
- package/dist/bin/diff-json-schema.d.ts.map +1 -1
- package/dist/bin/diff-json-schema.js +1 -4
- package/dist/bin/diff-json-schema.js.map +1 -1
- package/dist/bin/git.d.ts +26 -34
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +60 -143
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/main.js +126 -140
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/notify.d.ts +35 -0
- package/dist/bin/notify.d.ts.map +1 -0
- package/dist/bin/notify.js +131 -0
- package/dist/bin/notify.js.map +1 -0
- package/dist/bin/run-tests.d.ts +21 -0
- package/dist/bin/run-tests.d.ts.map +1 -0
- package/dist/bin/run-tests.js +149 -0
- package/dist/bin/run-tests.js.map +1 -0
- package/dist/bin/slack.d.ts +2 -14
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +17 -46
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/stdin-input.d.ts +79 -0
- package/dist/bin/stdin-input.d.ts.map +1 -0
- package/dist/bin/stdin-input.js +124 -0
- package/dist/bin/stdin-input.js.map +1 -0
- package/dist/bin/test-files-glob.d.ts +18 -0
- package/dist/bin/test-files-glob.d.ts.map +1 -0
- package/dist/bin/test-files-glob.js +108 -0
- package/dist/bin/test-files-glob.js.map +1 -0
- package/dist/bin/test-report.d.ts +69 -8
- package/dist/bin/test-report.d.ts.map +1 -1
- package/dist/bin/test-report.js +76 -67
- package/dist/bin/test-report.js.map +1 -1
- package/dist/bin/types.d.ts +4 -7
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/bin/utils/config/load-config.d.ts +8 -0
- package/dist/bin/utils/config/load-config.d.ts.map +1 -1
- package/dist/bin/utils/config/load-config.js +14 -1
- package/dist/bin/utils/config/load-config.js.map +1 -1
- package/dist/bin/utils/config/parser.d.ts +15 -2
- package/dist/bin/utils/config/parser.d.ts.map +1 -1
- package/dist/bin/utils/config/parser.js +29 -8
- package/dist/bin/utils/config/parser.js.map +1 -1
- package/dist/bin/utils/config/structures/grouped.js +2 -2
- package/dist/bin/utils/config/structures/grouped.js.map +1 -1
- package/dist/bin/utils/config/structures/legacy.js +1 -1
- package/dist/bin/utils/config/structures/legacy.js.map +1 -1
- package/dist/lib/extend-expect.d.ts.map +1 -1
- package/dist/lib/extend-expect.js +37 -218
- package/dist/lib/extend-expect.js.map +1 -1
- package/dist/lib/lib.d.ts +1 -5
- package/dist/lib/lib.d.ts.map +1 -1
- package/dist/lib/lib.js +19 -31
- package/dist/lib/lib.js.map +1 -1
- package/dist/lib/run-checks.d.ts +14 -0
- package/dist/lib/run-checks.d.ts.map +1 -0
- package/dist/lib/run-checks.js +82 -0
- package/dist/lib/run-checks.js.map +1 -0
- package/dist/lib/run-test-result.d.ts +3 -6
- package/dist/lib/run-test-result.d.ts.map +1 -1
- package/dist/lib/run-test-result.js +41 -54
- package/dist/lib/run-test-result.js.map +1 -1
- package/dist/test/bundle/cli-bundle.test.js +76 -56
- package/dist/test/bundle/cli-bundle.test.js.map +1 -1
- package/dist/test/bundle/token-wrapper.test.d.ts +2 -0
- package/dist/test/bundle/token-wrapper.test.d.ts.map +1 -0
- package/dist/test/bundle/token-wrapper.test.js +80 -0
- package/dist/test/bundle/token-wrapper.test.js.map +1 -0
- package/dist/test/e2e/cli-bundle.e2e.test.js +33 -68
- package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
- package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
- package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
- package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
- package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
- package/dist/test/e2e/helpers.d.ts +37 -0
- package/dist/test/e2e/helpers.d.ts.map +1 -0
- package/dist/test/e2e/helpers.js +57 -0
- package/dist/test/e2e/helpers.js.map +1 -0
- package/dist/test/unit/bin/actor-filtering.test.js +7 -0
- package/dist/test/unit/bin/actor-filtering.test.js.map +1 -1
- package/dist/test/unit/bin/actor-ids.test.d.ts +2 -0
- package/dist/test/unit/bin/actor-ids.test.d.ts.map +1 -0
- package/dist/test/unit/bin/actor-ids.test.js +40 -0
- package/dist/test/unit/bin/actor-ids.test.js.map +1 -0
- package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/affected-actors-cli.test.js +224 -0
- package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
- package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
- package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
- package/dist/test/unit/bin/affected-actors.test.js +153 -0
- package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
- package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/build-cli.test.js +49 -0
- package/dist/test/unit/bin/build-cli.test.js.map +1 -0
- package/dist/test/unit/bin/build-from-local.test.js +12 -0
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
- package/dist/test/unit/bin/build.test.js +116 -6
- package/dist/test/unit/bin/build.test.js.map +1 -1
- package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
- package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
- package/dist/test/unit/bin/compiled-cli.js +23 -0
- package/dist/test/unit/bin/compiled-cli.js.map +1 -0
- package/dist/test/unit/bin/diff-changes.test.js +168 -114
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/diff.test.js +25 -28
- package/dist/test/unit/bin/diff.test.js.map +1 -1
- package/dist/test/unit/bin/git.test.js +67 -288
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/notify.test.d.ts +2 -0
- package/dist/test/unit/bin/notify.test.d.ts.map +1 -0
- package/dist/test/unit/bin/notify.test.js +222 -0
- package/dist/test/unit/bin/notify.test.js.map +1 -0
- package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
- package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
- package/dist/test/unit/bin/stdin-input.test.js +92 -0
- package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
- package/dist/test/unit/bin/test-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/test-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/test-cli.test.js +165 -0
- package/dist/test/unit/bin/test-cli.test.js.map +1 -0
- package/dist/test/unit/bin/test-files-glob.test.d.ts +2 -0
- package/dist/test/unit/bin/test-files-glob.test.d.ts.map +1 -0
- package/dist/test/unit/bin/test-files-glob.test.js +102 -0
- package/dist/test/unit/bin/test-files-glob.test.js.map +1 -0
- package/dist/test/unit/bin/test-report.test.d.ts +2 -0
- package/dist/test/unit/bin/test-report.test.d.ts.map +1 -0
- package/dist/test/unit/bin/test-report.test.js +100 -0
- package/dist/test/unit/bin/test-report.test.js.map +1 -0
- package/dist/test/unit/bin/utils/config/load-config.test.js +19 -1
- package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/parser.test.js +36 -4
- package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/structures/grouped.test.js +6 -0
- package/dist/test/unit/bin/utils/config/structures/grouped.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/structures/legacy.test.js +1 -0
- package/dist/test/unit/bin/utils/config/structures/legacy.test.js.map +1 -1
- package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts +2 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts.map +1 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.js +50 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.js.map +1 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts +2 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts.map +1 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js +102 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js.map +1 -0
- package/dist/test/unit/run-checks.test.d.ts +2 -0
- package/dist/test/unit/run-checks.test.d.ts.map +1 -0
- package/dist/test/unit/run-checks.test.js +62 -0
- package/dist/test/unit/run-checks.test.js.map +1 -0
- package/dist/test/unit/run-test-result.test.d.ts +2 -0
- package/dist/test/unit/run-test-result.test.d.ts.map +1 -0
- package/dist/test/unit/run-test-result.test.js +34 -0
- package/dist/test/unit/run-test-result.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +41 -38
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts +3 -0
- package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts.map +1 -0
- package/dist/workflow-scripts-src/run-with-apify-tokens.js +100 -0
- package/dist/workflow-scripts-src/run-with-apify-tokens.js.map +1 -0
- package/docs/cli.md +473 -0
- package/docs/lib.md +276 -0
- package/docs/workflows.md +455 -0
- package/eslint.config.mjs +10 -1
- package/lib/extend-expect.ts +48 -258
- package/lib/lib.ts +25 -34
- package/lib/run-checks.ts +101 -0
- package/lib/run-test-result.ts +45 -55
- package/package.json +4 -2
- package/platform-tests-investigate-and-fix/skills/assess-impact/SKILL.md +42 -0
- package/platform-tests-investigate-and-fix/skills/measure-flakiness/SKILL.md +4 -2
- package/test/bundle/cli-bundle.test.ts +97 -67
- package/test/bundle/token-wrapper.test.ts +110 -0
- package/test/e2e/cli-bundle.e2e.test.ts +60 -93
- package/test/e2e/git-build-release.e2e.test.ts +113 -0
- package/test/e2e/helpers.ts +82 -0
- package/test/unit/bin/actor-filtering.test.ts +8 -0
- package/test/unit/bin/actor-ids.test.ts +47 -0
- package/test/unit/bin/affected-actors-cli.test.ts +271 -0
- package/test/unit/bin/affected-actors.test.ts +221 -0
- package/test/unit/bin/build-cli.test.ts +61 -0
- package/test/unit/bin/build-from-local.test.ts +16 -0
- package/test/unit/bin/build.test.ts +149 -5
- package/test/unit/bin/compiled-cli.ts +23 -0
- package/test/unit/bin/diff-changes.test.ts +181 -114
- package/test/unit/bin/diff.test.ts +25 -28
- package/test/unit/bin/git.test.ts +76 -361
- package/test/unit/bin/notify.test.ts +299 -0
- package/test/unit/bin/stdin-input.test.ts +120 -0
- package/test/unit/bin/test-cli.test.ts +207 -0
- package/test/unit/bin/test-files-glob.test.ts +118 -0
- package/test/unit/bin/test-report.test.ts +118 -0
- package/test/unit/bin/utils/config/load-config.test.ts +26 -1
- package/test/unit/bin/utils/config/parser.test.ts +46 -5
- package/test/unit/bin/utils/config/structures/grouped.test.ts +12 -0
- package/test/unit/bin/utils/config/structures/legacy.test.ts +1 -0
- package/test/unit/github-scripts/mask-config-secrets.test.ts +61 -0
- package/test/unit/github-scripts/run-with-apify-tokens.test.ts +115 -0
- package/test/unit/run-checks.test.ts +76 -0
- package/test/unit/run-test-result.test.ts +44 -0
- package/test/unit/should-built-and-test.test.ts +46 -39
- package/tsconfig.json +1 -1
- package/vitest.config.ts +9 -0
- package/workflow-scripts-src/run-with-apify-tokens.ts +112 -0
- package/.github/scripts/check-major-tag-refs.mjs +0 -72
- package/dist/test/unit/bin/logging.test.d.ts +0 -2
- package/dist/test/unit/bin/logging.test.d.ts.map +0 -1
- package/dist/test/unit/bin/logging.test.js +0 -43
- package/dist/test/unit/bin/logging.test.js.map +0 -1
- package/dist/test/unit/bin/slack.test.d.ts +0 -2
- package/dist/test/unit/bin/slack.test.d.ts.map +0 -1
- package/dist/test/unit/bin/slack.test.js +0 -54
- package/dist/test/unit/bin/slack.test.js.map +0 -1
- package/test/unit/bin/logging.test.ts +0 -52
- package/test/unit/bin/slack.test.ts +0 -65
package/bin/run-tests.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import { createInterface } from 'node:readline';
|
|
8
|
+
|
|
9
|
+
import { withActorIds } from './actor-ids.js';
|
|
10
|
+
import { logger } from './logger.js';
|
|
11
|
+
import type { TestTarget } from './stdin-input.js';
|
|
12
|
+
import { resolveTestFilesGlob } from './test-files-glob.js';
|
|
13
|
+
import { failedRunReport, logTestReport, type TestReport, toTestReport, type VitestJsonReport } from './test-report.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Finds the vitest the repository installed, the way Node would resolve it from `cwd`. The CLI ships as a bundle
|
|
17
|
+
* that runs outside the repository's `node_modules`, so the tests keep the vitest (and the lib) version the
|
|
18
|
+
* repository locked. Read from `package.json` because vitest's exports don't expose its bin.
|
|
19
|
+
*/
|
|
20
|
+
const resolveVitestBin = async (cwd: string): Promise<string> => {
|
|
21
|
+
for (let dir = path.resolve(cwd); ; dir = path.dirname(dir)) {
|
|
22
|
+
const packageDir = path.join(dir, 'node_modules', 'vitest');
|
|
23
|
+
if (existsSync(path.join(packageDir, 'package.json'))) {
|
|
24
|
+
const { bin } = JSON.parse(await fs.readFile(path.join(packageDir, 'package.json'), 'utf8'));
|
|
25
|
+
return path.join(packageDir, typeof bin === 'string' ? bin : bin.vitest);
|
|
26
|
+
}
|
|
27
|
+
if (path.dirname(dir) === dir) {
|
|
28
|
+
throw new Error(`vitest is not installed in ${cwd}, install the repository's dependencies first`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Turns the glob into vitest filters. Vitest filters are parts of a file path, not globs, so the glob is resolved here.
|
|
35
|
+
* The filters are the folders that hold only matched files and the other matched files one by one, so the command
|
|
36
|
+
* line stays short. They go to vitest as absolute paths. Vitest also matches their path relative to the root anywhere
|
|
37
|
+
* in a test file's path, so a copy at `dist/test/platform/a.test.ts` would run too: vitest's own `exclude` keeps such
|
|
38
|
+
* copies out.
|
|
39
|
+
*/
|
|
40
|
+
const resolveVitestFilters = async (testFilesGlob: string, cwd: string): Promise<string[]> => {
|
|
41
|
+
const { files, paths } = await resolveTestFilesGlob(testFilesGlob, cwd);
|
|
42
|
+
// Without filters vitest would run every test of the repo
|
|
43
|
+
if (files.length === 0) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`No files match --test-files-glob "${testFilesGlob}". The glob is relative to the repo root, e.g. "test/platform/**".`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
logger.info(`Running the ${files.length} file(s) that match --test-files-glob "${testFilesGlob}"`);
|
|
49
|
+
logger.debug(`Files: ${files.join(', ')}`);
|
|
50
|
+
// Vitest compares against real paths, and cwd can be a symlink (e.g. /tmp on macOS). join keeps a folder's
|
|
51
|
+
// trailing slash, which stops `test/platform/daily/` from also matching `test/platform/daily-without-pr/`.
|
|
52
|
+
const root = await fs.realpath(cwd);
|
|
53
|
+
return paths.map((filePath) => path.join(root, filePath));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
interface RunTestsOptions {
|
|
57
|
+
/** Glob from the repo root (`cwd`) of the test files to run, e.g. `test/platform/**` */
|
|
58
|
+
testFilesGlob: string;
|
|
59
|
+
/** The Actors to test, each with its build or, without one, its default build */
|
|
60
|
+
targets: TestTarget[];
|
|
61
|
+
/** Keeps vitest's own JSON report there, e.g. to upload it */
|
|
62
|
+
vitestOutputFile?: string;
|
|
63
|
+
/** Vitest reporters to add to the default one and the JSON one the report is read from, e.g. `github-actions` */
|
|
64
|
+
reporters?: string[];
|
|
65
|
+
cwd?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Runs the repository's vitest on the given builds and returns what happened as a `TestReport`. Vitest's own output
|
|
70
|
+
* goes to stderr, so stdout stays free for the report. A failing test or a crashing vitest is a failed report, not
|
|
71
|
+
* an error: the report is how the failure gets to `notify`.
|
|
72
|
+
*/
|
|
73
|
+
export const runTests = async ({
|
|
74
|
+
testFilesGlob,
|
|
75
|
+
targets,
|
|
76
|
+
vitestOutputFile,
|
|
77
|
+
reporters = [],
|
|
78
|
+
cwd = process.cwd(),
|
|
79
|
+
}: RunTestsOptions): Promise<TestReport> => {
|
|
80
|
+
// Without it the lib skips every platform test, and a run that tested nothing would pass
|
|
81
|
+
if (!process.env.TESTER_APIFY_TOKEN) {
|
|
82
|
+
throw new Error('TESTER_APIFY_TOKEN is not set. The platform tests need it to run the Actors.');
|
|
83
|
+
}
|
|
84
|
+
const vitestBin = await resolveVitestBin(cwd);
|
|
85
|
+
const vitestFilters = await resolveVitestFilters(testFilesGlob, cwd);
|
|
86
|
+
// A test only runs when the ID it names its Actor by is listed, so each Actor gets both its full name and raw ID
|
|
87
|
+
const actorBuilds = await withActorIds(targets);
|
|
88
|
+
|
|
89
|
+
const tempDir = vitestOutputFile ? null : await fs.mkdtemp(path.join(os.tmpdir(), 'apify-test-tools-'));
|
|
90
|
+
const outputFile = path.resolve(cwd, vitestOutputFile ?? path.join(tempDir!, 'vitest-report.json'));
|
|
91
|
+
// A report left over from an earlier run must not pass for this one's
|
|
92
|
+
await fs.rm(outputFile, { force: true });
|
|
93
|
+
|
|
94
|
+
// Stdin is the only source of what to test, so whatever the caller's environment holds doesn't leak into the tests
|
|
95
|
+
const env = { ...process.env };
|
|
96
|
+
// Vitest writes to a pipe, so it would drop colors that the terminal it ends up in shows
|
|
97
|
+
if (process.stderr.isTTY && env.FORCE_COLOR === undefined && env.NO_COLOR === undefined) env.FORCE_COLOR = '1';
|
|
98
|
+
// The lib runs the tests of the Actors listed here, each on the build given, or on its default build without one
|
|
99
|
+
// Lib versions before 0.9.0 read the IDs as actorName and actorId, so they are given under both names
|
|
100
|
+
env.ACTOR_BUILDS = JSON.stringify(
|
|
101
|
+
actorBuilds.map((build) => ({ ...build, actorName: build.actorFullName, actorId: build.actorRawId })),
|
|
102
|
+
);
|
|
103
|
+
// Lib versions before 0.10.0 skip every test in their `describe` unless this is set. Newer versions read
|
|
104
|
+
// TESTER_APIFY_TOKEN instead and ignore it. It only unlocks `describe`: which tests run still follows ACTOR_BUILDS.
|
|
105
|
+
env.RUN_PLATFORM_TESTS = '1';
|
|
106
|
+
// In those versions this runs the tests of every Actor, whatever ACTOR_BUILDS lists
|
|
107
|
+
delete env.RUN_ALL_PLATFORM_TESTS;
|
|
108
|
+
const described = actorBuilds.map(
|
|
109
|
+
({ actorFullName, actorRawId, buildNumber }) =>
|
|
110
|
+
`${actorFullName ?? actorRawId} (${buildNumber ?? 'default build'})`,
|
|
111
|
+
);
|
|
112
|
+
logger.info(`Testing ${targets.length} Actor(s): ${described.join(', ') || 'none'}`);
|
|
113
|
+
|
|
114
|
+
const args = [
|
|
115
|
+
vitestBin,
|
|
116
|
+
'run',
|
|
117
|
+
...vitestFilters,
|
|
118
|
+
'--reporter=default',
|
|
119
|
+
'--reporter=json',
|
|
120
|
+
...reporters.map((reporter) => `--reporter=${reporter}`),
|
|
121
|
+
`--outputFile.json=${outputFile}`,
|
|
122
|
+
// Platform tests mostly wait on Actor runs, not on the CPU, so they run far more at once than vitest's defaults
|
|
123
|
+
'--maxConcurrency=20',
|
|
124
|
+
'--fileParallelism',
|
|
125
|
+
'--maxWorkers=100',
|
|
126
|
+
];
|
|
127
|
+
logger.debug(`Running vitest: ${args.join(' ')}`);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const exitCode = await spawnVitest(args, { cwd, env });
|
|
131
|
+
let vitestReport: VitestJsonReport;
|
|
132
|
+
try {
|
|
133
|
+
vitestReport = JSON.parse(await fs.readFile(outputFile, 'utf8'));
|
|
134
|
+
} catch (error) {
|
|
135
|
+
logger.debug(`Could not read the vitest report: ${(error as Error).message}`);
|
|
136
|
+
const report = failedRunReport(
|
|
137
|
+
`Vitest exited with code ${exitCode} without a report, see its output above`,
|
|
138
|
+
);
|
|
139
|
+
logTestReport(report);
|
|
140
|
+
return report;
|
|
141
|
+
}
|
|
142
|
+
const report = toTestReport(vitestReport, { cwd, exitCode });
|
|
143
|
+
logTestReport(report);
|
|
144
|
+
const { total, skipped } = report.summary;
|
|
145
|
+
if (targets.length !== 0 && total !== 0 && skipped === total) {
|
|
146
|
+
logger.warn(
|
|
147
|
+
`Every test was skipped, although ${targets.length} Actor(s) were given. Check that the tests name ` +
|
|
148
|
+
'these Actors by their full name or raw ID.',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return report;
|
|
152
|
+
} finally {
|
|
153
|
+
if (tempDir) await fs.rm(tempDir, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Resolves with vitest's exit code. Signals we get (a cancelled CI job) are passed on, so vitest can clean up. */
|
|
158
|
+
const spawnVitest = async (args: string[], { cwd, env }: { cwd: string; env: NodeJS.ProcessEnv }) =>
|
|
159
|
+
new Promise<number>((resolve, reject) => {
|
|
160
|
+
// Vitest's stdout goes to our stderr: stdout is reserved for the report. It is piped through us rather than
|
|
161
|
+
// handed over, to leave out the JSON reporter's note about the temporary file it wrote.
|
|
162
|
+
const child = spawn(process.execPath, args, { cwd, env, stdio: ['ignore', 'pipe', 2] });
|
|
163
|
+
createInterface({ input: child.stdout!, crlfDelay: Infinity }).on('line', (line) => {
|
|
164
|
+
if (!line.startsWith('JSON report written to ')) process.stderr.write(`${line}\n`);
|
|
165
|
+
});
|
|
166
|
+
const forward = (signal: NodeJS.Signals) => child.kill(signal);
|
|
167
|
+
process.on('SIGINT', forward);
|
|
168
|
+
process.on('SIGTERM', forward);
|
|
169
|
+
const cleanup = () => {
|
|
170
|
+
process.off('SIGINT', forward);
|
|
171
|
+
process.off('SIGTERM', forward);
|
|
172
|
+
};
|
|
173
|
+
child.on('error', (error) => {
|
|
174
|
+
cleanup();
|
|
175
|
+
reject(new Error(`Failed to start vitest: ${error.message}`));
|
|
176
|
+
});
|
|
177
|
+
child.on('close', (code, signal) => {
|
|
178
|
+
cleanup();
|
|
179
|
+
if (signal) logger.warn(`Vitest was stopped by ${signal}`);
|
|
180
|
+
resolve(code ?? 1);
|
|
181
|
+
});
|
|
182
|
+
});
|
package/bin/slack.ts
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
import { WebClient } from '@slack/web-api';
|
|
2
2
|
|
|
3
3
|
import { logger } from './logger.js';
|
|
4
|
-
import type { Commit } from './types.js';
|
|
5
4
|
import { getEnvVar } from './utils.js';
|
|
6
5
|
|
|
7
|
-
type NotifyToSlackOptions = {
|
|
8
|
-
repository: string;
|
|
9
|
-
changedFiles: string[];
|
|
10
|
-
changelog: string | null;
|
|
11
|
-
commits: Commit[];
|
|
12
|
-
dryRun: boolean;
|
|
13
|
-
author: string;
|
|
14
|
-
releaseSlackChannel?: string;
|
|
15
|
-
reportSlackChannel?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
6
|
/**
|
|
19
7
|
* Creates the client the real run posts with. A dry run posts nothing but still requires the token and checks
|
|
20
8
|
* Slack accepts it (`auth.test` is read-only), so it fails wherever the real run would.
|
|
@@ -34,75 +22,27 @@ export const getSlackClient = async (tokenEnvVar: string, dryRun: boolean): Prom
|
|
|
34
22
|
|
|
35
23
|
export const sendingLabel = (dryRun: boolean) => (dryRun ? '[DRY RUN] Would send' : 'Sending');
|
|
36
24
|
|
|
37
|
-
//
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
export const notifyToSlack = async ({
|
|
41
|
-
changedFiles,
|
|
42
|
-
commits,
|
|
43
|
-
changelog,
|
|
44
|
-
repository,
|
|
45
|
-
dryRun,
|
|
46
|
-
author,
|
|
47
|
-
releaseSlackChannel,
|
|
48
|
-
reportSlackChannel,
|
|
49
|
-
}: NotifyToSlackOptions) => {
|
|
50
|
-
if (!changelog) {
|
|
51
|
-
logger.warn('No new changelog entries found, did you forget to update it?');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!releaseSlackChannel && !reportSlackChannel) {
|
|
55
|
-
logger.info('Skipping slack notification, no --release-slack-channel or --report-slack-channel passed');
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const slack = await getSlackClient('SLACK_TOKEN_RELEASES_BOT', dryRun);
|
|
59
|
-
|
|
60
|
-
let shortMessage = `*${repository}* – New release (by ${displayName(author)}):\n\n`;
|
|
61
|
-
|
|
62
|
-
// This one is just for broader public that only cares about public facing changes
|
|
63
|
-
if (changelog && releaseSlackChannel) {
|
|
64
|
-
shortMessage += `*Additions to the changelog*:\n\n${changelog}\n`;
|
|
65
|
-
logger.info(`=========================================`);
|
|
66
|
-
logger.info(`**${sendingLabel(dryRun)} slack message to channel**: ${releaseSlackChannel}.\n\n${shortMessage}`);
|
|
67
|
-
logger.info(`=========================================`);
|
|
68
|
-
if (!dryRun) {
|
|
69
|
-
await slack.chat.postMessage({
|
|
70
|
-
channel: releaseSlackChannel,
|
|
71
|
-
text: shortMessage,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const commitsMessage = `${commits
|
|
77
|
-
.map(
|
|
78
|
-
({ author: commitAuthor, message }, index) =>
|
|
79
|
-
`${index + 1}. Commit message: ${message}\n\tAuthor: ${displayName(commitAuthor)}.`,
|
|
80
|
-
)
|
|
81
|
-
.join('\n')}`;
|
|
82
|
-
const changedFilesMessage = `*Files changed*: ${changedFiles.map((file) => `\`${file}\``).join(', ')}`;
|
|
83
|
-
const longMessage = `${shortMessage}\n*Commit list*:\n${commitsMessage}\n\n${changedFilesMessage}`;
|
|
84
|
-
|
|
85
|
-
// This one is for devs and project managers that need to know more details
|
|
86
|
-
if (reportSlackChannel) {
|
|
87
|
-
logger.info(`=========================================`);
|
|
88
|
-
logger.info(`${sendingLabel(dryRun)} slack message to channel: ${reportSlackChannel}.\n\n${longMessage}`);
|
|
89
|
-
logger.info(`=========================================`);
|
|
90
|
-
if (!dryRun) {
|
|
91
|
-
await slack.chat.postMessage({
|
|
92
|
-
text: longMessage,
|
|
93
|
-
channel: reportSlackChannel,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
};
|
|
25
|
+
// Slack's limits: 50 blocks per message, 3000 characters per section
|
|
26
|
+
const MAX_BLOCKS_PER_MESSAGE = 50;
|
|
27
|
+
const MAX_SECTION_LENGTH = 3000;
|
|
98
28
|
|
|
99
|
-
|
|
29
|
+
/** Posts `text`, with `details` as sections in its thread, as many replies as Slack's block limit needs. */
|
|
30
|
+
export const sendSlackMessage = async (slack: WebClient, channel: string, text: string, details: string[]) => {
|
|
100
31
|
const { ts } = await slack.chat.postMessage({ text, channel });
|
|
101
|
-
if (
|
|
32
|
+
if (!ts) return;
|
|
33
|
+
for (let i = 0; i < details.length; i += MAX_BLOCKS_PER_MESSAGE) {
|
|
34
|
+
const sections = details.slice(i, i + MAX_BLOCKS_PER_MESSAGE);
|
|
102
35
|
await slack.chat.postMessage({
|
|
103
36
|
channel,
|
|
104
37
|
thread_ts: ts,
|
|
105
|
-
|
|
38
|
+
text: sections.join('\n').slice(0, MAX_SECTION_LENGTH),
|
|
39
|
+
blocks: sections.map((t) => ({
|
|
40
|
+
text: {
|
|
41
|
+
type: 'mrkdwn',
|
|
42
|
+
text: t.length > MAX_SECTION_LENGTH ? `${t.slice(0, MAX_SECTION_LENGTH - 1)}…` : t,
|
|
43
|
+
},
|
|
44
|
+
type: 'section',
|
|
45
|
+
})),
|
|
106
46
|
});
|
|
107
47
|
}
|
|
108
48
|
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
|
|
3
|
+
import z, { prettifyError, type ZodType } from 'zod';
|
|
4
|
+
|
|
5
|
+
export const readStdin = async (): Promise<string> => {
|
|
6
|
+
// Nothing piped in: fail right away instead of waiting for someone to type
|
|
7
|
+
if (process.stdin.isTTY) {
|
|
8
|
+
throw new Error('Expected JSON on stdin, but nothing was piped in.');
|
|
9
|
+
}
|
|
10
|
+
let raw = '';
|
|
11
|
+
for await (const chunk of process.stdin) {
|
|
12
|
+
raw += chunk;
|
|
13
|
+
}
|
|
14
|
+
return raw;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const parseStdinJson = <T>(
|
|
18
|
+
raw: string,
|
|
19
|
+
schema: ZodType<T>,
|
|
20
|
+
{ expected, example }: { expected: string; example: string },
|
|
21
|
+
) => {
|
|
22
|
+
// Empty input is an error, not "nothing to do": it is what a failed command earlier in the pipe leaves behind
|
|
23
|
+
if (!raw.trim()) {
|
|
24
|
+
throw new Error(`Expected ${expected} as JSON on stdin, got nothing (${example}).`);
|
|
25
|
+
}
|
|
26
|
+
let json: unknown;
|
|
27
|
+
try {
|
|
28
|
+
json = JSON.parse(raw);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`The input on stdin is not valid JSON: ${(error as Error).message}`);
|
|
31
|
+
}
|
|
32
|
+
return validateStdinJson(json, schema, { expected, example });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const validateStdinJson = <T>(
|
|
36
|
+
json: unknown,
|
|
37
|
+
schema: ZodType<T>,
|
|
38
|
+
{ expected, example }: { expected: string; example: string },
|
|
39
|
+
) => {
|
|
40
|
+
const result = schema.safeParse(json);
|
|
41
|
+
if (!result.success) {
|
|
42
|
+
throw new Error(`The input on stdin is not ${expected} (${example}):\n${prettifyError(result.error)}`);
|
|
43
|
+
}
|
|
44
|
+
return result.data;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What `build` reads: the Actors to build. Only their names are read, the config file stays the source of truth
|
|
49
|
+
* for everything else about them. Other fields are ignored, so a get-affected-actors document only needs its Actor
|
|
50
|
+
* list picked, e.g. `jq '{ actors: .functionallyChangedActors }'`.
|
|
51
|
+
*/
|
|
52
|
+
const buildInputSchema = z.object({
|
|
53
|
+
actors: z.array(z.object({ actorFullName: z.string() })),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const parseBuildInput = (raw: string) =>
|
|
57
|
+
parseStdinJson(raw, buildInputSchema, {
|
|
58
|
+
expected: 'the Actors to build',
|
|
59
|
+
example: `e.g. echo '{"actors":[{"actorFullName":"owner/name"}]}' | apify-test-tools build --version-number 0.1`,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What `test` reads: the Actors to test, each with the build to test or, without one, its default build. The builds
|
|
64
|
+
* `build` and `build-from-local` print fit, and so does `get-actor-configs`, whose other fields are ignored. An Actor
|
|
65
|
+
* needs its full name or its raw ID, `test` looks up the other. A dry-run plan is an object on purpose, so it is
|
|
66
|
+
* rejected here rather than tested as if it were built.
|
|
67
|
+
*/
|
|
68
|
+
const testTargetsSchema = z.array(
|
|
69
|
+
z
|
|
70
|
+
.object({
|
|
71
|
+
actorFullName: z.string().optional(),
|
|
72
|
+
buildId: z.string().optional(),
|
|
73
|
+
actorRawId: z.string().optional(),
|
|
74
|
+
buildNumber: z.string().optional(),
|
|
75
|
+
})
|
|
76
|
+
.refine(({ actorFullName, actorRawId }) => !!actorFullName || !!actorRawId, {
|
|
77
|
+
message: 'An Actor needs actorFullName or actorRawId',
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export type TestTarget = z.infer<typeof testTargetsSchema>[number];
|
|
82
|
+
|
|
83
|
+
const isDryRunPlan = (raw: string) => {
|
|
84
|
+
try {
|
|
85
|
+
return (JSON.parse(raw) as { dryRun?: unknown } | null)?.dryRun === true;
|
|
86
|
+
} catch {
|
|
87
|
+
// Not JSON at all, parseStdinJson says so
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const parseTestTargets = (raw: string) => {
|
|
93
|
+
const example =
|
|
94
|
+
'e.g. ... | apify-test-tools build --version-number 0.99 | apify-test-tools test, ' +
|
|
95
|
+
"or apify-test-tools get-actor-configs | apify-test-tools test for every Actor's default build";
|
|
96
|
+
if (isDryRunPlan(raw)) {
|
|
97
|
+
throw new Error(`The input on stdin is a dry-run plan, not builds, so there is nothing to test (${example}).`);
|
|
98
|
+
}
|
|
99
|
+
return parseStdinJson(raw, testTargetsSchema, { expected: 'the Actors to test', example });
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** The report `test` prints, see TestReport. */
|
|
103
|
+
const testRunSchema = z.object({ actorId: z.string(), runId: z.string(), runLink: z.string() });
|
|
104
|
+
const testReportSchema = z.object({
|
|
105
|
+
kind: z.literal('test-report'),
|
|
106
|
+
version: z.literal(1),
|
|
107
|
+
success: z.boolean(),
|
|
108
|
+
summary: z.object({ total: z.number(), passed: z.number(), failed: z.number(), skipped: z.number() }),
|
|
109
|
+
tests: z.array(
|
|
110
|
+
z.object({
|
|
111
|
+
name: z.string(),
|
|
112
|
+
file: z.string(),
|
|
113
|
+
status: z.enum(['passed', 'failed', 'skipped']),
|
|
114
|
+
durationMs: z.number().nullable(),
|
|
115
|
+
failureMessages: z.array(z.string()),
|
|
116
|
+
run: testRunSchema.nullable(),
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
errors: z.array(z.string()),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/** The parts of a get-affected-actors document that describe what a release changed. Other fields are ignored. */
|
|
123
|
+
const releaseChangesSchema = z.object({
|
|
124
|
+
commits: z.array(z.object({ sha: z.string(), author: z.string(), date: z.string(), message: z.string() })),
|
|
125
|
+
changelog: z.string().nullable(),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export type ReleaseChanges = z.infer<typeof releaseChangesSchema>;
|
|
129
|
+
|
|
130
|
+
export type NotifyInput =
|
|
131
|
+
| { kind: 'tests'; report: z.infer<typeof testReportSchema> }
|
|
132
|
+
| { kind: 'release'; changes: ReleaseChanges };
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* What `notify` reads: a test run, as `test` reports it, or a release, as `get-affected-actors` reports it. The test
|
|
136
|
+
* report says so in its `kind`, a release is told by its `commits`, so either can be piped in as it is.
|
|
137
|
+
*/
|
|
138
|
+
export const parseNotifyInput = (raw: string): NotifyInput => {
|
|
139
|
+
const example =
|
|
140
|
+
'e.g. ... | apify-test-tools test | apify-test-tools notify --slack-channel "#my-channel", ' +
|
|
141
|
+
'or apify-test-tools get-affected-actors --since <sha> | apify-test-tools notify --slack-channel "#my-channel"';
|
|
142
|
+
const expected = 'the output of test or get-affected-actors';
|
|
143
|
+
const json = parseStdinJson(raw, z.looseObject({}), { expected, example });
|
|
144
|
+
if (json.kind === 'test-report') {
|
|
145
|
+
const report = validateStdinJson(json, testReportSchema, { expected: 'the output of test', example });
|
|
146
|
+
return { kind: 'tests', report };
|
|
147
|
+
}
|
|
148
|
+
if ('commits' in json) {
|
|
149
|
+
const changes = validateStdinJson(json, releaseChangesSchema, {
|
|
150
|
+
expected: 'the output of get-affected-actors',
|
|
151
|
+
example,
|
|
152
|
+
});
|
|
153
|
+
return { kind: 'release', changes };
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`The input on stdin is not ${expected} (${example}).`);
|
|
156
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import picomatch from 'picomatch';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Test globs are from the repo root, e.g. `test/platform/**`. `get-affected-actors` matches changed files against the
|
|
8
|
+
* config's `platformTestsGlob` and `test` runs the files `--test-files-glob` matches, so both read globs the same way.
|
|
9
|
+
*/
|
|
10
|
+
const PICOMATCH_OPTIONS = { nocase: true, dot: true } as const;
|
|
11
|
+
|
|
12
|
+
/** Never walked: they hold no test files of the repo, and node_modules can be huge. */
|
|
13
|
+
const SKIPPED_DIRS = new Set(['node_modules', '.git']);
|
|
14
|
+
|
|
15
|
+
const assertRelativeGlob = (glob: string) => {
|
|
16
|
+
if (path.posix.isAbsolute(glob) || path.win32.isAbsolute(glob)) {
|
|
17
|
+
throw new Error(`Test file globs must be relative to the repo root, got "${glob}".`);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Tells whether a repo-root-relative path matches the glob. Without a glob, nothing matches. */
|
|
22
|
+
export const createTestFilesMatcher = (glob?: string): ((filePath: string) => boolean) => {
|
|
23
|
+
if (!glob) return () => false;
|
|
24
|
+
assertRelativeGlob(glob);
|
|
25
|
+
const isMatch = picomatch(glob, PICOMATCH_OPTIONS);
|
|
26
|
+
// One argument only: picomatch's second one returns a truthy object, and Array#filter passes the index there
|
|
27
|
+
return (filePath) => isMatch(filePath);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
interface Listing {
|
|
31
|
+
/** POSIX paths relative to `cwd` */
|
|
32
|
+
files: string[];
|
|
33
|
+
/** What wasn't walked (skipped directories, symlinks), so the folders holding them never count as fully matched */
|
|
34
|
+
skipped: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Lists the files under `dir`. A missing `dir` has no files. */
|
|
38
|
+
const listFiles = async (cwd: string, dir: string): Promise<Listing> => {
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = await fs.readdir(path.join(cwd, dir), { withFileTypes: true });
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const { code } = error as NodeJS.ErrnoException;
|
|
44
|
+
if (code === 'ENOENT') return { files: [], skipped: [] };
|
|
45
|
+
// A glob without wildcards has the file itself as its base
|
|
46
|
+
if (code === 'ENOTDIR') return { files: [dir], skipped: [] };
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
const listing: Listing = { files: [], skipped: [] };
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const entryPath = path.posix.join(dir, entry.name);
|
|
52
|
+
if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) {
|
|
53
|
+
const inner = await listFiles(cwd, entryPath);
|
|
54
|
+
listing.files.push(...inner.files);
|
|
55
|
+
listing.skipped.push(...inner.skipped);
|
|
56
|
+
} else if (entry.isFile()) {
|
|
57
|
+
listing.files.push(entryPath);
|
|
58
|
+
} else {
|
|
59
|
+
listing.skipped.push(entryPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return listing;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isWithin = (dir: string, base: string) => base === '.' || dir === base || dir.startsWith(`${base}/`);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The directories from `base` down to the file's own directory, `base` first. None when `base` is the file itself,
|
|
69
|
+
* as for a glob without wildcards.
|
|
70
|
+
*/
|
|
71
|
+
const dirsFromBase = (file: string, base: string): string[] => {
|
|
72
|
+
const dirs: string[] = [];
|
|
73
|
+
for (let dir = path.posix.dirname(file); isWithin(dir, base); dir = path.posix.dirname(dir)) {
|
|
74
|
+
dirs.unshift(dir);
|
|
75
|
+
if (dir === base || dir === '.') break;
|
|
76
|
+
}
|
|
77
|
+
return dirs;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Turns the matched files into as few paths as possible: a directory, with a trailing `/`, where the glob matches
|
|
82
|
+
* every file in it, and a single file otherwise. `test/platform/{daily,hourly}/**` gives two paths however many
|
|
83
|
+
* tests the folders hold, so the vitest command line stays short.
|
|
84
|
+
*/
|
|
85
|
+
const toCoveringPaths = ({ files, skipped }: Listing, matched: Set<string>, base: string): string[] => {
|
|
86
|
+
const counts = new Map<string, { total: number; matched: number }>();
|
|
87
|
+
for (const file of [...files, ...skipped]) {
|
|
88
|
+
for (const dir of dirsFromBase(file, base)) {
|
|
89
|
+
const count = counts.get(dir) ?? { total: 0, matched: 0 };
|
|
90
|
+
count.total += 1;
|
|
91
|
+
if (matched.has(file)) count.matched += 1;
|
|
92
|
+
counts.set(dir, count);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const isCovered = (dir: string) => counts.get(dir)!.total === counts.get(dir)!.matched;
|
|
96
|
+
const paths = new Set<string>();
|
|
97
|
+
for (const file of matched) {
|
|
98
|
+
// The highest directory that holds only matched files, so its children are not listed one by one
|
|
99
|
+
const coveredDir = dirsFromBase(file, base).find(isCovered);
|
|
100
|
+
paths.add(coveredDir === undefined ? file : `${coveredDir}/`);
|
|
101
|
+
}
|
|
102
|
+
return [...paths].sort();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
interface ResolvedTestFiles {
|
|
106
|
+
/** Every file the glob matches, as repo-root-relative POSIX paths, sorted */
|
|
107
|
+
files: string[];
|
|
108
|
+
/**
|
|
109
|
+
* The same files as few paths as possible: directories (ending with `/`) that hold only matched files, and the
|
|
110
|
+
* other matched files one by one. Sorted.
|
|
111
|
+
*/
|
|
112
|
+
paths: string[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Finds the files the glob matches. `cwd` is the repo root. Only the glob's static base (`test/platform` for
|
|
117
|
+
* `test/platform/{daily,hourly}/**`) is walked.
|
|
118
|
+
*/
|
|
119
|
+
export const resolveTestFilesGlob = async (glob: string, cwd: string): Promise<ResolvedTestFiles> => {
|
|
120
|
+
const isMatch = createTestFilesMatcher(glob);
|
|
121
|
+
// normalize turns `./test` into `test`, the way git prints paths
|
|
122
|
+
const base = path.posix.normalize(picomatch.scan(glob).base || '.');
|
|
123
|
+
const listing = await listFiles(cwd, base);
|
|
124
|
+
const files = listing.files.filter(isMatch).sort();
|
|
125
|
+
return { files, paths: toCoveringPaths(listing, new Set(files), base) };
|
|
126
|
+
};
|