@veda-hq/pariksha 0.5.17 → 0.5.19
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/README.md +209 -2
- package/dist/cli.js +154 -1
- package/dist/duplicate-tests.js +367 -0
- package/dist/flaky-tests.js +252 -0
- package/dist/hook-preamble.js +343 -0
- package/dist/masked-exit-codes.js +762 -0
- package/dist/shell-command.js +425 -0
- package/dist/shell-gates.js +283 -0
- package/dist/shell-pitfalls.js +408 -0
- package/dist/shell-sources.js +165 -0
- package/dist/shell-text.js +417 -22
- package/dist/vitest-json-report.js +129 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
test-quality ratchets. Part of [Veda](https://github.com/veda-hq/veda).
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
|
-
npx @veda-hq/pariksha@0.5.
|
|
8
|
-
npx @veda-hq/pariksha@0.5.
|
|
7
|
+
npx @veda-hq/pariksha@0.5.19 <check> [flags]
|
|
8
|
+
npx @veda-hq/pariksha@0.5.19 --version
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Every check exits 0 when the repository passes, 1 when it refuses something
|
|
@@ -467,6 +467,44 @@ config (an unknown, missing or bad key or pattern), a malformed baseline, no
|
|
|
467
467
|
`node_modules/.bin` (DOC-DRIFT-02 would know no bin), a missing or failing
|
|
468
468
|
lychee, a `--base` with no merge base, or a failed git call.
|
|
469
469
|
|
|
470
|
+
## `duplicate-tests`
|
|
471
|
+
|
|
472
|
+
```sh
|
|
473
|
+
pariksha duplicate-tests [--config <file>] [--files-from <file>|-] [--root <dir>]
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
A test copied rather than written: two tests whose matchers, names and
|
|
477
|
+
describe paths score at or above the threshold (default 0.85; across two
|
|
478
|
+
files never below 0.95). The score weighs the matcher set (`expect()` and
|
|
479
|
+
every `.to…()` call in the test, Jaccard) at 50%, the name at 20% and the
|
|
480
|
+
describe path at 30% (edit distance); `describe(X.name)` names its block `X`,
|
|
481
|
+
and two tests in no describe block compare their file stems. A pair is not
|
|
482
|
+
compared when either test has fewer than three matcher calls, either name is
|
|
483
|
+
generic (`should return`, …), the names are opposite cases (an opposite
|
|
484
|
+
pair, other HTTP methods or statuses, other uppercase `AND`/`OR`, another
|
|
485
|
+
endpoint), or, in one file, either name has a word the other lacks or the
|
|
486
|
+
same words come in another order.
|
|
487
|
+
|
|
488
|
+
Without `--files-from` every tracked file on disk is listed; with it, the
|
|
489
|
+
listed paths (one per line or NUL-separated, relative to the root or
|
|
490
|
+
absolute; `-` reads stdin). A listed path counts when `include` matches it
|
|
491
|
+
relative to the root. A file that cannot be read warns on stderr and is
|
|
492
|
+
skipped; a file that does not parse prints `❌ Parse error:` on stderr and
|
|
493
|
+
gives no tests. The report goes to stdout. The JSON `--config` (relative to
|
|
494
|
+
the root) replaces each default it names:
|
|
495
|
+
|
|
496
|
+
- `include` (default `\.(test|spec)\.(ts|tsx|js|jsx)$`): which paths are tests.
|
|
497
|
+
- `threshold` (default `0.85`).
|
|
498
|
+
- `commonNames`: names too generic to compare, lowercased.
|
|
499
|
+
- `oppositePairs`: `[word, word]` pairs that mark two names as opposite
|
|
500
|
+
cases (the default holds fifteen, such as `success`/`failure` and
|
|
501
|
+
`valid`/`invalid`). Names are lowercased before they are matched, so a pair
|
|
502
|
+
must be lowercase to match.
|
|
503
|
+
|
|
504
|
+
Needs `typescript` `>=5.4 <6`, as `coverage-by-type` does. A `.ts` file is
|
|
505
|
+
read as TypeScript and the rest as TSX; a `.ts` file that only parses as
|
|
506
|
+
TSX is read so, and one with a `<T>value` assertion gives no tests.
|
|
507
|
+
|
|
470
508
|
## `enforcement-claims`
|
|
471
509
|
|
|
472
510
|
```sh
|
|
@@ -740,6 +778,88 @@ root-relative POSIX path, a check not in `checks`, an `exemptIn` group
|
|
|
740
778
|
baseline, a root holding no tracked file, a group discovering nothing, no
|
|
741
779
|
base without `--no-base`, a failed git call, or no `typescript`.
|
|
742
780
|
|
|
781
|
+
## `flaky-tests`
|
|
782
|
+
|
|
783
|
+
```sh
|
|
784
|
+
pariksha flaky-tests [--config <file>] [--files-from <file>|-] [--runs <n>] [--root <dir>]
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
Runs the test files `--runs` times (default 5, at most 1000) with
|
|
788
|
+
`npx vitest run --reporter=json --passWithNoTests`, the files under each
|
|
789
|
+
package root together from that root and the rest from the repository root,
|
|
790
|
+
and judges every run's JSON report strictly (a report that breaks Vitest's
|
|
791
|
+
contract, or a clean exit with no report or one saying `success: false`, is
|
|
792
|
+
not trusted). The verdicts, each exiting 1 but the last:
|
|
793
|
+
|
|
794
|
+
1. a run exited non-zero without collecting a test case (an unloadable file,
|
|
795
|
+
a crashed runner): the runner output is printed, and any flaky test seen
|
|
796
|
+
in the other runs below it;
|
|
797
|
+
2. a test passed in some runs and failed in others: flaky;
|
|
798
|
+
3. every run failed, or a test failed in every run: not flaky, but broken;
|
|
799
|
+
4. every run passed: `✅ No flaky tests found across N test cases.`
|
|
800
|
+
|
|
801
|
+
The files are listed as for `duplicate-tests`. The JSON `--config` (relative
|
|
802
|
+
to the root) replaces each default it names:
|
|
803
|
+
|
|
804
|
+
- `include` (default `\.(test|spec)\.(ts|tsx|js|jsx)$`): which paths are tests.
|
|
805
|
+
- `packageRoots` (default none): directories, relative to the root, whose
|
|
806
|
+
files Vitest runs from that directory, with its config.
|
|
807
|
+
- `exclude` (default none): globs passed to Vitest as `--exclude`.
|
|
808
|
+
|
|
809
|
+
## `masked-exit-codes`
|
|
810
|
+
|
|
811
|
+
```sh
|
|
812
|
+
pariksha masked-exit-codes [--config <file>] [--staged] [--root <dir>]
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
A gate that keeps reporting success after it stopped checking passes
|
|
816
|
+
everything: a hook that runs a commit-message linter and then `exit 0`, a
|
|
817
|
+
review step whose failures read as "no issues found", a baseline read with
|
|
818
|
+
`2>/dev/null || true`. `|| true` has honest uses (an optional tool, grep's
|
|
819
|
+
"no matches"), so this refuses only the shapes that silence a gate command's
|
|
820
|
+
verdict:
|
|
821
|
+
|
|
822
|
+
1. `exit 0` at the end of a script that ran a gate, with no branch on the
|
|
823
|
+
gate's status;
|
|
824
|
+
2. `|| true`, a `||` handler that does not exit non-zero, or `2>/dev/null`
|
|
825
|
+
with the status unchecked, on a gate command, also in a script the line
|
|
826
|
+
hands a shell (`eval '…'`, `bash -c '…'`, what `echo`/`printf` pipes
|
|
827
|
+
into `sh`) and inside `$( … )`;
|
|
828
|
+
3. in a file `pipefailScope` matches, `echo`/`printf` of a variable piped
|
|
829
|
+
into a reader that stops early (`grep -q`, `-m`, `-l`, `head`): under
|
|
830
|
+
pipefail the writer can fail after the match, which reads as a miss.
|
|
831
|
+
|
|
832
|
+
The files are those git tracks under the root ending `.sh` or under a hook
|
|
833
|
+
directory, outside `node_modules/`, read from the working tree, or the index
|
|
834
|
+
with `--staged`. Comments, quoted text, heredoc bodies and continuation lines
|
|
835
|
+
are read as bash reads them. The JSON `--config` (relative to the root) is
|
|
836
|
+
optional:
|
|
837
|
+
|
|
838
|
+
```json
|
|
839
|
+
{
|
|
840
|
+
"id": "SH-001",
|
|
841
|
+
"gateCommands": ["eslint", "tsc", "vitest", "prettier --check", "commitlint", "run-validation.sh"],
|
|
842
|
+
"hookDirs": [".husky/"],
|
|
843
|
+
"pipefailScope": ["^\\.husky/", "^scripts/lib/.+\\.sh$"]
|
|
844
|
+
}
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
- `id` (default `masked-exit-codes`) names the check in its messages.
|
|
848
|
+
- `gateCommands` (default `eslint`, `tsc`, `vitest`, `prettier --check`,
|
|
849
|
+
`commitlint`, `cubic review`, `gitleaks`, `actionlint`, `shellcheck`,
|
|
850
|
+
`depcruise`, `stryker`) are the commands whose status is a verdict; one of
|
|
851
|
+
several words must appear whole. Keep it narrow: a broad list fires on
|
|
852
|
+
every optional-tool probe.
|
|
853
|
+
- `hookDirs` (default `.husky/`) are path prefixes whose files are shell
|
|
854
|
+
whatever their name.
|
|
855
|
+
- `pipefailScope` (default `^\.husky/`) are path regexes: the files that run
|
|
856
|
+
under `set -o pipefail`.
|
|
857
|
+
|
|
858
|
+
A clean run prints `✅ <id>: no masked exit codes (N shell file(s) scanned)`
|
|
859
|
+
to stdout; each finding goes to stderr with its line and why, and exits 1.
|
|
860
|
+
No shell file at all exits 1 (`the file lookup failed`), as the original
|
|
861
|
+
did; a bad config or a failed git call exits 2.
|
|
862
|
+
|
|
743
863
|
## `non-synced-resources`
|
|
744
864
|
|
|
745
865
|
```sh
|
|
@@ -869,6 +989,93 @@ Parsing those is the job of a command guard such as kavach's `git_safety`.
|
|
|
869
989
|
project's settings and then also skips the user's own `defaultMode`, so it
|
|
870
990
|
belongs in user settings only.
|
|
871
991
|
|
|
992
|
+
## `shell-gates`
|
|
993
|
+
|
|
994
|
+
```sh
|
|
995
|
+
pariksha shell-gates [--config <file>] [--staged | --all | --base <ref> | --files-from <file>|-] [--only <checks>] [--root <dir>]
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
A shell script that works on its writer's Mac can fail on Linux CI, and a
|
|
999
|
+
git hook that picks up whatever `node` is on PATH can run a check under the
|
|
1000
|
+
wrong runtime. This runs three gates over the shell files a change writes:
|
|
1001
|
+
|
|
1002
|
+
1. ShellCheck, a binary on PATH (install it: <https://www.shellcheck.net>),
|
|
1003
|
+
once per pass, by default `--severity=warning`, then `--severity=info
|
|
1004
|
+
--include=SC2086,SC2013,SC2015` (an unquoted expansion, `for x in $(cat
|
|
1005
|
+
f)`, `a && b || c`). Its targets are `*.sh`, `*.bash`, the hooks
|
|
1006
|
+
directory's direct children without an extension, and extensionless
|
|
1007
|
+
files whose first line is a sh, bash, dash or ksh `#!`. Staged mode
|
|
1008
|
+
writes the index's copy of each to a temporary directory and judges
|
|
1009
|
+
that; base mode judges the HEAD commit's copy of what the branch changed
|
|
1010
|
+
since its merge base with `<ref>`; `--all` judges the working tree; files
|
|
1011
|
+
mode passes the listed paths as they are.
|
|
1012
|
+
2. The hook preamble, over the hooks directory's direct children without an
|
|
1013
|
+
extension. A hook must source `selectScript` and call `selectFunction`
|
|
1014
|
+
(with `|| exit 1`, or a branch that exits) before it runs a runtime
|
|
1015
|
+
(`runtimes`, by default `node`, `npx`, `pnpm`, `npm`, `tsx`, `corepack`,
|
|
1016
|
+
`yarn`), here or in a function it calls; an opt-out (`[ "$X" = 0 ] &&
|
|
1017
|
+
exit 0`) comes before that selection; it never edits PATH or drives
|
|
1018
|
+
nvm, fnm or mise itself; it never `exec sh <hook>`s (that ignores the
|
|
1019
|
+
target's shebang); and an INT, TERM, HUP or QUIT trap's handler ends in
|
|
1020
|
+
`exit`, or bash resumes after it and a cancelled hook carries on.
|
|
1021
|
+
3. The pitfalls, over every shell target: `grep -E` or `egrep` with `\s`,
|
|
1022
|
+
`\S`, `\d` or `\D` (not POSIX ERE: BSD grep matches nothing); GNU-only
|
|
1023
|
+
`xargs -d`/`-r`; `xargs` without `-0` fed a file list; file operands
|
|
1024
|
+
after `xargs`, or a list expansion to grep, without `--`; `<( … )` in a
|
|
1025
|
+
file `validatorPaths` matches; a literal `/tmp/<name>` (use `mktemp`);
|
|
1026
|
+
an unguarded `/opt/homebrew` or `/Users/` literal, a fixed `-avd
|
|
1027
|
+
<name>`, a personal device name; `NVM_DIR` set without honouring an
|
|
1028
|
+
existing one; `lockf` without `-k`; `$(( … ))` over a value from `read`
|
|
1029
|
+
or `date` without `10#` (`08` is bad octal); `dirname "$0"` beside
|
|
1030
|
+
`BASH_SOURCE`; `git worktree list` parsed without `--porcelain`; and
|
|
1031
|
+
`grep -o` over a JSON `"key":`. Comments, quoted text and heredoc bodies
|
|
1032
|
+
are read as bash reads them.
|
|
1033
|
+
|
|
1034
|
+
The preamble and pitfalls judge the same kind of target, less `exclude`
|
|
1035
|
+
and symlinks: `--staged` (the default) the staged copies, `--base` the
|
|
1036
|
+
files changed since the merge base with `<ref>` (read from the working
|
|
1037
|
+
tree), `--all` every tracked one, `--files-from` the listed paths (one per
|
|
1038
|
+
line or NUL-separated; a file, or `-` for stdin). A change under
|
|
1039
|
+
`gateInputs` (staged, or on the branch) re-judges every target, and a
|
|
1040
|
+
staged change to a `shellcheck.inputs` path makes ShellCheck judge the
|
|
1041
|
+
whole index. `--only preamble,pitfalls` runs
|
|
1042
|
+
only those checks, and no ShellCheck. The JSON `--config` (relative to the
|
|
1043
|
+
root) is optional:
|
|
1044
|
+
|
|
1045
|
+
```json
|
|
1046
|
+
{
|
|
1047
|
+
"hooksDir": ".husky/",
|
|
1048
|
+
"exclude": ["node_modules/", ".husky/_/", "vendor/"],
|
|
1049
|
+
"gateInputs": ["scripts/validations/shell/"],
|
|
1050
|
+
"preamble": { "id": "SHELL-06", "selectScript": "scripts/lib/select-node.sh", "selectFunction": "select_node" },
|
|
1051
|
+
"pitfalls": { "id": "SHELL-03", "validatorPaths": ["^scripts/validations/"] },
|
|
1052
|
+
"shellcheck": { "inputs": ["scripts/run-validation.sh"], "install": "run the machine setup (or: brew install shellcheck)" },
|
|
1053
|
+
"guidance": ["", " SHELL-06 and SHELL-03 are described in docs/shell.md."]
|
|
1054
|
+
}
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
- `hooksDir` (default `.husky/`, ending in `/`) is the git hooks directory.
|
|
1058
|
+
- `exclude` (default `node_modules/`, `.husky/_/`) are path prefixes never
|
|
1059
|
+
judged by the preamble and pitfalls; a `/node_modules/` segment never is.
|
|
1060
|
+
- `gateInputs` (default none) are path prefixes of the gate's own inputs.
|
|
1061
|
+
- `preamble` (default none, which leaves only the pitfalls to run):
|
|
1062
|
+
`selectScript` and `selectFunction` are required; `id` defaults to
|
|
1063
|
+
`hook-preamble`, `runtimes` to the list above.
|
|
1064
|
+
- `pitfalls`: `id` (default `shell-pitfalls`) and `validatorPaths` (path
|
|
1065
|
+
regexes, default none).
|
|
1066
|
+
- `shellcheck`: `passes` (lists of flags after `--norc`; `[]` runs no
|
|
1067
|
+
ShellCheck), `inputs` (default none) and `install`, the text a missing
|
|
1068
|
+
ShellCheck message gives.
|
|
1069
|
+
- `guidance` (default a pointer to this section) is printed after the
|
|
1070
|
+
findings.
|
|
1071
|
+
|
|
1072
|
+
ShellCheck's own report comes first, then `✅ Shell gates: N file(s) pass
|
|
1073
|
+
<checks>`, or `❌ Shell gates: N finding(s) in M file(s)` on stderr with
|
|
1074
|
+
each finding as `file:line <id> <rule>` and its message, then the
|
|
1075
|
+
guidance. It exits 1 on any ShellCheck or rule finding, or when ShellCheck
|
|
1076
|
+
is not installed; 2 on a bad config, two modes, an empty list, or a
|
|
1077
|
+
selection or git failure, since nothing was checked.
|
|
1078
|
+
|
|
872
1079
|
## markdownlint rules
|
|
873
1080
|
|
|
874
1081
|
```jsonc
|
package/dist/cli.js
CHANGED
|
@@ -7,11 +7,13 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { checkAbsolutePaths, DEFAULT_GUIDANCE as ABSOLUTE_PATHS_GUIDANCE } from "./absolute-paths.js";
|
|
8
8
|
import { checkGates, DEFAULT_GATE_CONFIG, parseGateConfig as parseAggregateConfig } from "./aggregate-gate-needs.js";
|
|
9
9
|
import { parseDocConfig, readDoc, selectDocs } from "./doc-files.js";
|
|
10
|
+
import { DEFAULT_MASKED_CONFIG, findMaskedExits, formatMasked, parseMaskedConfig, readerFor as maskedReaderFor, shellFiles } from "./masked-exit-codes.js";
|
|
10
11
|
import { checkNonSyncedResources, DEFAULT_GUIDANCE as NON_SYNCED_GUIDANCE } from "./non-synced-resources.js";
|
|
12
|
+
import { ALL_CHECKS as SHELL_CHECKS, parseGatesConfig, runShellGates, shellcheckRunner } from "./shell-gates.js";
|
|
11
13
|
import { checkPnpmSettings, DEFAULT_PNPM_CONFIG, formatParity, parsePnpmConfig } from "./pnpm-settings.js";
|
|
12
14
|
import { adrDirOf, DEFAULTS, findUncited, formatClaims, listAdrs, readOrUndefined, stagedSource, treeSource } from "./authority-citations.js";
|
|
13
15
|
import { findDenied, hash, isText, parseDenyList } from "./boundary.js";
|
|
14
|
-
import { DEFAULT_CONFIG, findGaps, formatGaps, listedPaths, parseConfig, treePaths } from "./coverage-by-type.js";
|
|
16
|
+
import { DEFAULT_CONFIG, entries, findGaps, formatGaps, listedPaths, parseConfig, treePaths } from "./coverage-by-type.js";
|
|
15
17
|
import { checkBuild, checkPackage, checkVitest, formatViolations, loaderFailure, loadVitestExclude, missingConfigs, ownedBy, parseConfig as parseExemptions } from "./coverage-exemptions.js";
|
|
16
18
|
import { checkBudgets } from "./doc-budgets.js";
|
|
17
19
|
import { lycheeRunner } from "./doc-links.js";
|
|
@@ -27,6 +29,8 @@ import { discover as discoverCodecs, fileOf as codecFileOf, findings as codecFin
|
|
|
27
29
|
import { discoverGroup, findings as faultFindings, formatFindings as formatFaultFindings, parseBaseline as parseFaultBaseline, parseConfig as parseFaultConfig } from "./fault-injection-registry.js";
|
|
28
30
|
import { emptyRoots, growthSince, readBaseline, wholeId } from "./registry-ratchet.js";
|
|
29
31
|
import { checkClaims, formatOutcome, loadEslint, parseConfig as parseClaimsConfig } from "./enforcement-claims.js";
|
|
32
|
+
import { parseConfig as parseDuplicateConfig, runDuplicateTests } from "./duplicate-tests.js";
|
|
33
|
+
import { DEFAULT_RUNS, npxVitest, parseConfig as parseFlakyConfig, parseRuns, runFlakyTests } from "./flaky-tests.js";
|
|
30
34
|
export const DENY_FILE = '.veda/deny.sha256';
|
|
31
35
|
const USAGE = [
|
|
32
36
|
'usage: pariksha authority-citations [--mode staged|tree] [--authority <a,b>] [--issue-prefix <A,B>] [--adr-dir <dir>] [--root <dir>]',
|
|
@@ -38,13 +42,17 @@ const USAGE = [
|
|
|
38
42
|
' pariksha coverage-exemptions --config <file> [--package <dir>] [--root <dir>]',
|
|
39
43
|
' pariksha doc-budgets --table <file> [--section <name>] [--root <dir>]',
|
|
40
44
|
' pariksha doc-references --config <file> (--staged | --base <ref> [--head <ref>] | --all [--update-baseline | --write-baseline]) [--checks <ids>] [--root <dir>]',
|
|
45
|
+
' pariksha duplicate-tests [--config <file>] [--files-from <file>|-] [--root <dir>]',
|
|
41
46
|
' pariksha enforcement-claims --config <file> [--root <dir>]',
|
|
42
47
|
' pariksha env-gate-switches --config <file> [--mode staged|tree] [--root <dir>]',
|
|
43
48
|
' pariksha export-deletions [--base <ref>] [--root <dir>]',
|
|
44
49
|
' pariksha fault-injection-registry --config <file> [--base <ref> | --no-base] [--root <dir>]',
|
|
50
|
+
' pariksha flaky-tests [--config <file>] [--files-from <file>|-] [--runs <n>] [--root <dir>]',
|
|
51
|
+
' pariksha masked-exit-codes [--config <file>] [--staged] [--root <dir>]',
|
|
45
52
|
' pariksha non-synced-resources [--config <file>] [--files-from <file>|-] [--root <dir>]',
|
|
46
53
|
' pariksha pnpm-settings [--config <file>] [--root <dir>]',
|
|
47
54
|
' pariksha required-settings --require <file> [--settings <file>] [--root <dir>]',
|
|
55
|
+
' pariksha shell-gates [--config <file>] [--staged | --all | --base <ref> | --files-from <file>|-] [--only <checks>] [--root <dir>]',
|
|
48
56
|
' pariksha hash <word>',
|
|
49
57
|
' pariksha --version',
|
|
50
58
|
].join('\n');
|
|
@@ -120,6 +128,76 @@ const nonSyncedResources = docCheck('non-synced-resources', NON_SYNCED_GUIDANCE,
|
|
|
120
128
|
throw new Error(`${base} is not in a git repository`);
|
|
121
129
|
return top;
|
|
122
130
|
}, config));
|
|
131
|
+
/** Standard input, or the file `from` names, read whole. */
|
|
132
|
+
const listFrom = (from, io) => (from === '-' ? (io.stdin ?? (() => readFileSync(0, 'utf8')))() : readFileSync(resolve(from), 'utf8'));
|
|
133
|
+
/**
|
|
134
|
+
* masked-exit-codes: the shell files git tracks under `--root`, read from
|
|
135
|
+
* the working tree, or the index with `--staged`. The report goes to stdout
|
|
136
|
+
* when clean and stderr when not, as the original printed it.
|
|
137
|
+
*/
|
|
138
|
+
const maskedExitCodes = (f, io) => {
|
|
139
|
+
const root = resolve(f.get('--root') ?? '.');
|
|
140
|
+
try {
|
|
141
|
+
const configPath = f.get('--config');
|
|
142
|
+
const config = configPath === undefined ? DEFAULT_MASKED_CONFIG : parseMaskedConfig(readFileSync(resolve(root, configPath), 'utf8'));
|
|
143
|
+
const files = shellFiles(root, config);
|
|
144
|
+
const problems = files.length === 0 ? [] : findMaskedExits(root, files, maskedReaderFor(f.has('--staged')), config);
|
|
145
|
+
const result = formatMasked(problems, files.length, config.id);
|
|
146
|
+
result.out.forEach(io.out);
|
|
147
|
+
result.err.forEach(io.err);
|
|
148
|
+
return result.ok ? 0 : 1;
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
io.err(`pariksha masked-exit-codes: ${e.message}; nothing was checked, so this is not a pass`);
|
|
152
|
+
return 2;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* shell-gates: one of `--staged` (the default), `--all`, `--base <ref>` or
|
|
157
|
+
* `--files-from <file>|-`; `--only` names the checks to run, and then
|
|
158
|
+
* ShellCheck sits the run out, as the original did.
|
|
159
|
+
*/
|
|
160
|
+
const shellGates = (f, io) => {
|
|
161
|
+
const P = '❌ Shell gates could not run:';
|
|
162
|
+
const root = resolve(f.get('--root') ?? '.');
|
|
163
|
+
const modes = ['--staged', '--all', '--base', '--files-from'].filter((name) => f.has(name));
|
|
164
|
+
if (modes.length > 1) {
|
|
165
|
+
io.err(`${P} choose one of --staged, --all, --base, --files-from (got ${modes.join(', ')})`);
|
|
166
|
+
return 2;
|
|
167
|
+
}
|
|
168
|
+
let config;
|
|
169
|
+
let mode;
|
|
170
|
+
try {
|
|
171
|
+
const configPath = f.get('--config');
|
|
172
|
+
config = parseGatesConfig(configPath === undefined ? '{}' : readFileSync(resolve(root, configPath), 'utf8'));
|
|
173
|
+
const base = f.get('--base');
|
|
174
|
+
const from = f.get('--files-from');
|
|
175
|
+
if (from !== undefined) {
|
|
176
|
+
const files = entries(listFrom(from, io));
|
|
177
|
+
if (files.length === 0)
|
|
178
|
+
throw new Error('the --files-from list names no file, and a check of nothing is no pass');
|
|
179
|
+
mode = { kind: 'files', files };
|
|
180
|
+
}
|
|
181
|
+
else
|
|
182
|
+
mode = base !== undefined ? { kind: 'base', base } : f.has('--all') ? { kind: 'all' } : { kind: 'staged' };
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
io.err(`${P} ${e.message}`);
|
|
186
|
+
return 2;
|
|
187
|
+
}
|
|
188
|
+
const only = f.get('--only');
|
|
189
|
+
let checks = SHELL_CHECKS.filter((name) => name !== 'preamble' || config.preamble !== undefined);
|
|
190
|
+
if (only !== undefined) {
|
|
191
|
+
const names = only.split(',').filter(Boolean);
|
|
192
|
+
const unknown = names.filter((name) => !SHELL_CHECKS.some((check) => check === name));
|
|
193
|
+
if (names.length === 0 || unknown.length > 0) {
|
|
194
|
+
io.err(`${P} --only takes ${SHELL_CHECKS.join(',')}; got ${names.join(',') || 'nothing'}`);
|
|
195
|
+
return 2;
|
|
196
|
+
}
|
|
197
|
+
checks = SHELL_CHECKS.filter((name) => names.includes(name));
|
|
198
|
+
}
|
|
199
|
+
return runShellGates({ root, mode, checks, only: only !== undefined }, config, { out: io.out, err: io.err, shellcheck: io.shellcheck ?? shellcheckRunner() });
|
|
200
|
+
};
|
|
123
201
|
const aggregateGateNeeds = (f, io) => {
|
|
124
202
|
const root = resolve(f.get('--root') ?? '.');
|
|
125
203
|
const configPath = f.get('--config');
|
|
@@ -620,6 +698,77 @@ const requiredSettings = (f, io) => {
|
|
|
620
698
|
result.err.forEach(io.err);
|
|
621
699
|
return result.ok ? 0 : 1;
|
|
622
700
|
};
|
|
701
|
+
/**
|
|
702
|
+
* The test files duplicate-tests and flaky-tests judge, relative to the root:
|
|
703
|
+
* those `--files-from` lists (mode `files`; a relative entry is read from the
|
|
704
|
+
* root), or every tracked file on disk (mode `all`), kept when `include`
|
|
705
|
+
* matches the root-relative path.
|
|
706
|
+
*/
|
|
707
|
+
const testFiles = (f, io, root, include) => {
|
|
708
|
+
const from = f.get('--files-from');
|
|
709
|
+
const listed = from === undefined
|
|
710
|
+
? must(gitAt(root), ['ls-files', '-z'])
|
|
711
|
+
.split('\0')
|
|
712
|
+
.filter((p) => p && existsSync(join(root, p)))
|
|
713
|
+
: entries(from === '-' ? (io.stdin ?? (() => readFileSync(0, 'utf8')))() : readFileSync(resolve(from), 'utf8')).map((e) => relative(root, resolve(root, e)));
|
|
714
|
+
return { mode: from === undefined ? 'all' : 'files', files: listed.filter((p) => include.test(p)) };
|
|
715
|
+
};
|
|
716
|
+
/** duplicate-tests: the report on stdout, warnings and parse errors on stderr, as the original printed them. */
|
|
717
|
+
const duplicateTests = (f, io) => {
|
|
718
|
+
const P = 'pariksha duplicate-tests:';
|
|
719
|
+
const dir = resolve(f.get('--root') ?? '.');
|
|
720
|
+
const root = topOf(dir);
|
|
721
|
+
if (root === undefined) {
|
|
722
|
+
io.err(`${P} ${dir} is not in a git repository`);
|
|
723
|
+
return 2;
|
|
724
|
+
}
|
|
725
|
+
const ts = (io.typescript ?? (() => loadTypescript(typescriptLoaders(root))))();
|
|
726
|
+
if (typeof ts === 'string') {
|
|
727
|
+
io.err(`${P} ${ts}`);
|
|
728
|
+
return 2;
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
const configPath = f.get('--config');
|
|
732
|
+
const config = parseDuplicateConfig(configPath === undefined ? undefined : readFileSync(resolve(root, configPath), 'utf8'));
|
|
733
|
+
const { mode, files } = testFiles(f, io, root, config.include);
|
|
734
|
+
const listed = files.map((file) => ({ file, path: resolve(root, file), read: () => readFileSync(resolve(root, file), 'utf8') }));
|
|
735
|
+
const result = runDuplicateTests(ts, config, mode, listed);
|
|
736
|
+
result.out.forEach(io.out);
|
|
737
|
+
result.err.forEach(io.err);
|
|
738
|
+
return result.code;
|
|
739
|
+
}
|
|
740
|
+
catch (e) {
|
|
741
|
+
io.err(`${P} ${e.message}; nothing was checked, so this is not a pass`);
|
|
742
|
+
return 2;
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
/** flaky-tests: `--runs` passes (default 5) of Vitest over the test files, from each package root. */
|
|
746
|
+
const flakyTests = (f, io) => {
|
|
747
|
+
const P = 'pariksha flaky-tests:';
|
|
748
|
+
const runsText = f.get('--runs');
|
|
749
|
+
const runs = runsText === undefined ? DEFAULT_RUNS : parseRuns(runsText);
|
|
750
|
+
if (runs === null) {
|
|
751
|
+
io.err('--runs must be an integer from 1 to 1000');
|
|
752
|
+
return 2;
|
|
753
|
+
}
|
|
754
|
+
const dir = resolve(f.get('--root') ?? '.');
|
|
755
|
+
const root = topOf(dir);
|
|
756
|
+
if (root === undefined) {
|
|
757
|
+
io.err(`${P} ${dir} is not in a git repository`);
|
|
758
|
+
return 2;
|
|
759
|
+
}
|
|
760
|
+
let selected;
|
|
761
|
+
try {
|
|
762
|
+
const configPath = f.get('--config');
|
|
763
|
+
const config = parseFlakyConfig(configPath === undefined ? undefined : readFileSync(resolve(root, configPath), 'utf8'));
|
|
764
|
+
selected = { config, ...testFiles(f, io, root, config.include) };
|
|
765
|
+
}
|
|
766
|
+
catch (e) {
|
|
767
|
+
io.err(`${P} ${e.message}; nothing was checked, so this is not a pass`);
|
|
768
|
+
return 2;
|
|
769
|
+
}
|
|
770
|
+
return runFlakyTests(selected.config, runs, selected.mode, root, selected.files, io.vitest ?? npxVitest, io.out);
|
|
771
|
+
};
|
|
623
772
|
const COMMANDS = {
|
|
624
773
|
'codec-registry': { allowed: ['--config', '--base', '--root'], switches: ['--no-base'], run: codecRegistry },
|
|
625
774
|
'coverage-exemptions': { allowed: ['--config', '--package', '--root'], run: coverageExemptions },
|
|
@@ -633,10 +782,14 @@ const COMMANDS = {
|
|
|
633
782
|
'coverage-by-type': { allowed: ['--config', '--files-from', '--root'], run: coverageByType },
|
|
634
783
|
'doc-budgets': { allowed: ['--table', '--section', '--root'], run: docBudgets },
|
|
635
784
|
'doc-references': { allowed: ['--config', '--base', '--head', '--checks', '--root'], switches: ['--staged', '--all', '--update-baseline', '--write-baseline'], run: docReferences },
|
|
785
|
+
'duplicate-tests': { allowed: ['--config', '--files-from', '--root'], run: duplicateTests },
|
|
636
786
|
'enforcement-claims': { allowed: ['--config', '--root'], run: enforcementClaims },
|
|
787
|
+
'flaky-tests': { allowed: ['--config', '--files-from', '--runs', '--root'], run: flakyTests },
|
|
637
788
|
'env-gate-switches': { allowed: ['--config', '--mode', '--root'], run: envGateSwitches },
|
|
638
789
|
'export-deletions': { allowed: ['--base', '--root'], run: exportDeletions },
|
|
790
|
+
'masked-exit-codes': { allowed: ['--config', '--root'], switches: ['--staged'], run: maskedExitCodes },
|
|
639
791
|
'required-settings': { allowed: ['--require', '--settings', '--root'], run: requiredSettings },
|
|
792
|
+
'shell-gates': { allowed: ['--config', '--base', '--files-from', '--only', '--root'], switches: ['--staged', '--all'], run: shellGates },
|
|
640
793
|
};
|
|
641
794
|
/** The exit code; a Promise of it for the one check that is asynchronous (enforcement-claims, which loads ESLint). */
|
|
642
795
|
export const run = (args, io, version = ownVersion(), env = process.env) => {
|