@binary-balance/seshat 0.1.0-rc.1 → 0.2.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/README.md +145 -5
- package/bin/seshat.mjs +36 -35
- package/package.json +12 -8
package/README.md
CHANGED
|
@@ -1,8 +1,148 @@
|
|
|
1
1
|
# Seshat
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
the current Node platform. The native payload packages are optional so npm can
|
|
5
|
-
skip the four platforms that do not match the consumer.
|
|
3
|
+
> Seshat was the ancient Egyptian goddess of writing, wisdom, and knowledge. Seen as a scribe and record keeper, she became identified as the goddess of measurement, accounting, architecture, science, astronomy, mathematics, geometry, history and surveying.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
A native code-assurance tool for TypeScript and TSX, written in Rust. Seshat
|
|
6
|
+
combines function-level [CRAP](https://testing.googleblog.com/2011/02/this-code-is-crap.html)
|
|
7
|
+
analysis with [comparison-operator mutation testing](https://en.wikipedia.org/wiki/Mutation_testing) to find complex and/or
|
|
8
|
+
insufficiently tested code.
|
|
9
|
+
|
|
10
|
+
## What Seshat does
|
|
11
|
+
|
|
12
|
+
- Calculates function complexity and CRAP scores from statement coverage.
|
|
13
|
+
- Changes comparison operators one at a time and checks whether tests detect them.
|
|
14
|
+
- Runs tests in isolated copies of the captured project.
|
|
15
|
+
- Produces readable terminal output and versioned JSON, with optional CI thresholds.
|
|
16
|
+
- Supports explicit source selection, multiple test setups and parallel mutation workers.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install --save-dev @binary-balance/seshat
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Seshat needs Node `>=24.20.0 <25`. npm installs a prebuilt binary for Linux
|
|
25
|
+
x64/ARM64 (glibc), macOS x64/ARM64 or Windows x64, so Rust is not required.
|
|
26
|
+
Alpine Linux, which uses musl instead of glibc, is not supported. See the
|
|
27
|
+
[platform support matrix](https://github.com/Binary-Balance/seshat/blob/main/docs/platform-support.md) for details.
|
|
28
|
+
|
|
29
|
+
The project being assessed must already have its own dependencies installed,
|
|
30
|
+
including TypeScript, its test runner and a coverage tool.
|
|
31
|
+
|
|
32
|
+
## Supported test runners
|
|
33
|
+
|
|
34
|
+
| Runner | Supported versions |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| Node's built-in test runner | Node `>=24.20.0 <25` |
|
|
37
|
+
| Vitest | `>=5.0.0 <6` |
|
|
38
|
+
| Jest with Expo | Jest `29.7.0` with jest-expo `57.0.5` |
|
|
39
|
+
|
|
40
|
+
Seshat checks the runner version on every run and stops if it is not
|
|
41
|
+
supported. See the [runner compatibility guide](https://github.com/Binary-Balance/seshat/blob/main/docs/runner-compatibility.md).
|
|
42
|
+
|
|
43
|
+
## Configure
|
|
44
|
+
|
|
45
|
+
Add a `seshat.json` to the project root. This one is for Node's built-in test
|
|
46
|
+
runner:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"source": {
|
|
51
|
+
"include": ["src/**/*.ts"]
|
|
52
|
+
},
|
|
53
|
+
"capture": [
|
|
54
|
+
"package.json", "package-lock.json", "tsconfig.json", "collect-node.mjs",
|
|
55
|
+
"src", "tests", "node_modules"
|
|
56
|
+
],
|
|
57
|
+
"setups": [{
|
|
58
|
+
"name": "node",
|
|
59
|
+
"runner": "node",
|
|
60
|
+
"cwd": ".",
|
|
61
|
+
"typecheck": ["node", "node_modules/typescript/bin/tsc", "--project", "tsconfig.json"],
|
|
62
|
+
"test": [
|
|
63
|
+
"node", "--test", "--test-concurrency=1",
|
|
64
|
+
"--test-reporter={seshatReporter}", "tests/rules.test.mjs"
|
|
65
|
+
],
|
|
66
|
+
"coverage": {
|
|
67
|
+
"command": ["node", "collect-node.mjs", "tests/rules.test.mjs"],
|
|
68
|
+
"report": "coverage/coverage-final.json"
|
|
69
|
+
}
|
|
70
|
+
}]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `source` selects the files to assess, using glob patterns.
|
|
75
|
+
- `capture` lists the files and directories copied into each isolated run. It
|
|
76
|
+
must include the selected source and everything the commands need.
|
|
77
|
+
- `setups` gives the typecheck, test and coverage commands. The coverage
|
|
78
|
+
command must write an Istanbul JSON report; `collect-node.mjs` is a small
|
|
79
|
+
adapter from the [Node example](https://github.com/Binary-Balance/seshat/tree/main/examples/node).
|
|
80
|
+
|
|
81
|
+
The [examples](https://github.com/Binary-Balance/seshat/blob/main/examples/README.md) include complete Node, npm workspace, Vitest
|
|
82
|
+
and Jest/Expo projects. The [configuration guide](https://github.com/Binary-Balance/seshat/blob/main/docs/configuration.md) covers
|
|
83
|
+
every field, including `workers` for parallel mutation runs.
|
|
84
|
+
|
|
85
|
+
## Use
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
npx @binary-balance/seshat check # CRAP and mutation testing
|
|
89
|
+
npx @binary-balance/seshat crap # coverage and CRAP only
|
|
90
|
+
npx @binary-balance/seshat mutate # mutation testing only
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Use the full package name with `npx`. A bare `npx seshat` can fetch an
|
|
94
|
+
unrelated package of that name.
|
|
95
|
+
|
|
96
|
+
Each command reads `./seshat.json`; use `--config PATH` for another file. Add
|
|
97
|
+
`--json` to write a report to stdout, as described in the
|
|
98
|
+
[report format](https://github.com/Binary-Balance/seshat/blob/main/docs/report-format.md). `--help` lists all options.
|
|
99
|
+
|
|
100
|
+
Run only trusted test commands. Seshat's copies protect the project checkout,
|
|
101
|
+
but tests can still access external files, services, databases and credentials.
|
|
102
|
+
|
|
103
|
+
## Understand the results
|
|
104
|
+
|
|
105
|
+
CRAP uses complexity and statement coverage (a fraction from 0 to 1):
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
complexity^2 * (1 - coverage)^3 + complexity
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For complexity 10, zero coverage gives `110`, 50% coverage gives `22.5` and
|
|
112
|
+
full coverage gives `10`.
|
|
113
|
+
|
|
114
|
+
The mutation score is the percentage of mutants that made a test fail. A mutant
|
|
115
|
+
is one changed comparison, such as `>` to `>=`. If tests catch two of three
|
|
116
|
+
mutants, the score is `66.67%`. A timeout or execution error leaves the run
|
|
117
|
+
incomplete and withholds the score. Neither score proves correctness.
|
|
118
|
+
|
|
119
|
+
## Use in CI
|
|
120
|
+
|
|
121
|
+
Add optional thresholds to `seshat.json`:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
"thresholds": {
|
|
125
|
+
"maxCrap": 30,
|
|
126
|
+
"minMutationScore": 80
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
| Exit status | Meaning |
|
|
131
|
+
| ---: | --- |
|
|
132
|
+
| `0` | Complete run, thresholds met. |
|
|
133
|
+
| `1` | Complete run, a threshold was not met. |
|
|
134
|
+
| `2` | Invalid input, failed baseline or incomplete run. |
|
|
135
|
+
| `130` / `143` | Cancelled by `SIGINT` / `SIGTERM` on Unix. |
|
|
136
|
+
|
|
137
|
+
See [quality thresholds](https://github.com/Binary-Balance/seshat/blob/main/docs/configuration.md#quality-thresholds) for the full
|
|
138
|
+
rules.
|
|
139
|
+
|
|
140
|
+
## Development
|
|
141
|
+
|
|
142
|
+
See the [architecture](https://github.com/Binary-Balance/seshat/blob/main/docs/architecture.md), the
|
|
143
|
+
[build and regression guide](https://github.com/Binary-Balance/seshat/blob/main/benchmarks/proofs/README.md) and the
|
|
144
|
+
[packaging guide](https://github.com/Binary-Balance/seshat/blob/main/packaging/README.md).
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
[MIT](https://github.com/Binary-Balance/seshat/blob/main/LICENSE).
|
package/bin/seshat.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {spawn} from 'node:child_process';
|
|
4
4
|
import {createRequire} from 'node:module';
|
|
5
|
+
import {constants} from 'node:os';
|
|
5
6
|
import {existsSync, readFileSync, statSync} from 'node:fs';
|
|
6
7
|
import {basename, dirname, join} from 'node:path';
|
|
7
8
|
import {fileURLToPath} from 'node:url';
|
|
@@ -11,6 +12,8 @@ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
|
11
12
|
const packageManifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
|
|
12
13
|
const version = packageManifest.version;
|
|
13
14
|
const args = process.argv.slice(2);
|
|
15
|
+
// Match native argument errors: no -- separator or --flag=value syntax exists,
|
|
16
|
+
// and path values cannot start with --. A literal --json always requests JSON.
|
|
14
17
|
const jsonRequested = args.includes('--json');
|
|
15
18
|
const commands = new Set(['check', 'crap', 'mutate']);
|
|
16
19
|
const started = performance.now();
|
|
@@ -87,10 +90,6 @@ function payloadExecutable(target) {
|
|
|
87
90
|
return executable;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
function signalNumber(signal) {
|
|
91
|
-
return {SIGINT: 2, SIGTERM: 15}[signal] ?? 1;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
93
|
const host = targetForHost();
|
|
95
94
|
if (host.error) {
|
|
96
95
|
fail(host.error);
|
|
@@ -102,37 +101,39 @@ if (host.error) {
|
|
|
102
101
|
fail(error instanceof Error ? error.message : String(error));
|
|
103
102
|
}
|
|
104
103
|
if (executable) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
};
|
|
116
|
-
// A CTRL_BREAK_EVENT is broadcast to the console process group. Keep the
|
|
117
|
-
// launcher alive on Windows so the native child can handle that event and
|
|
118
|
-
// return its cancellation report through inherited stdio.
|
|
119
|
-
const preserveConsole = () => {};
|
|
120
|
-
process.on('SIGINT', forward);
|
|
121
|
-
process.on('SIGTERM', forward);
|
|
122
|
-
if (process.platform === 'win32') process.on('SIGBREAK', preserveConsole);
|
|
123
|
-
const cleanup = () => {
|
|
124
|
-
process.removeListener('SIGINT', forward);
|
|
125
|
-
process.removeListener('SIGTERM', forward);
|
|
126
|
-
if (process.platform === 'win32') process.removeListener('SIGBREAK', preserveConsole);
|
|
127
|
-
};
|
|
128
|
-
child.once('error', error => {
|
|
129
|
-
spawnFailed = true;
|
|
130
|
-
cleanup();
|
|
104
|
+
let child;
|
|
105
|
+
try {
|
|
106
|
+
child = spawn(executable, args, {
|
|
107
|
+
cwd: process.cwd(),
|
|
108
|
+
env: {...process.env},
|
|
109
|
+
shell: false,
|
|
110
|
+
windowsHide: false,
|
|
111
|
+
stdio: 'inherit',
|
|
112
|
+
});
|
|
113
|
+
} catch (error) {
|
|
131
114
|
fail(`cannot start native payload: ${error.message}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
115
|
+
}
|
|
116
|
+
if (child) {
|
|
117
|
+
let spawnFailed = false;
|
|
118
|
+
const forward = signal => {
|
|
119
|
+
if (!spawnFailed && child.exitCode === null && child.signalCode === null) child.kill(signal);
|
|
120
|
+
};
|
|
121
|
+
// Windows delivers Ctrl+C/Break to the shared console. Forwarding SIGINT
|
|
122
|
+
// with child.kill() force-terminates the native child before it can clean up.
|
|
123
|
+
const preserveConsole = () => {};
|
|
124
|
+
const interrupt = process.platform === 'win32' ? preserveConsole : forward;
|
|
125
|
+
process.on('SIGINT', interrupt);
|
|
126
|
+
process.on('SIGTERM', forward);
|
|
127
|
+
if (process.platform === 'win32') process.on('SIGBREAK', preserveConsole);
|
|
128
|
+
// Keep handlers until launcher exit: a second console/group signal can arrive
|
|
129
|
+
// after child close and must not replace the child's already chosen exit code.
|
|
130
|
+
child.once('error', error => {
|
|
131
|
+
spawnFailed = true;
|
|
132
|
+
fail(`cannot start native payload: ${error.message}`);
|
|
133
|
+
});
|
|
134
|
+
child.once('close', (code, signal) => {
|
|
135
|
+
if (!spawnFailed) process.exitCode = signal ? 128 + (constants.signals[signal] ?? 1) : code ?? 1;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
137
138
|
}
|
|
138
139
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@binary-balance/seshat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A native CLI for TypeScript and TSX complexity analysis and mutation testing.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Binary-Balance/seshat.git"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"engines": {
|
|
8
|
-
"node": "24.20.0"
|
|
12
|
+
"node": ">=24.20.0 <25"
|
|
9
13
|
},
|
|
10
14
|
"bin": {
|
|
11
15
|
"seshat": "bin/seshat.mjs"
|
|
@@ -16,10 +20,10 @@
|
|
|
16
20
|
"LICENSE"
|
|
17
21
|
],
|
|
18
22
|
"optionalDependencies": {
|
|
19
|
-
"@binary-balance/seshat-linux-x64": "0.
|
|
20
|
-
"@binary-balance/seshat-linux-arm64": "0.
|
|
21
|
-
"@binary-balance/seshat-darwin-x64": "0.
|
|
22
|
-
"@binary-balance/seshat-darwin-arm64": "0.
|
|
23
|
-
"@binary-balance/seshat-win32-x64": "0.
|
|
23
|
+
"@binary-balance/seshat-linux-x64": "0.2.0",
|
|
24
|
+
"@binary-balance/seshat-linux-arm64": "0.2.0",
|
|
25
|
+
"@binary-balance/seshat-darwin-x64": "0.2.0",
|
|
26
|
+
"@binary-balance/seshat-darwin-arm64": "0.2.0",
|
|
27
|
+
"@binary-balance/seshat-win32-x64": "0.2.0"
|
|
24
28
|
}
|
|
25
29
|
}
|