vitest-bats 0.0.1
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/LICENSE +21 -0
- package/README.md +415 -0
- package/bats-executor.js +103 -0
- package/bats-generator.js +114 -0
- package/coverage-reporter.js +319 -0
- package/index.d.ts +655 -0
- package/index.js +9 -0
- package/matchers.js +310 -0
- package/package.json +68 -0
- package/plugin.js +208 -0
- package/runtime.d.ts +82 -0
- package/runtime.js +172 -0
- package/schema.js +75 -0
- package/setup.d.ts +1 -0
- package/setup.js +12 -0
- package/tsdoc-metadata.json +11 -0
package/runtime.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { executeBats } from "./bats-executor.js";
|
|
2
|
+
import { generateBatsFile } from "./bats-generator.js";
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
|
|
7
|
+
//#region src/runtime.ts
|
|
8
|
+
/**
|
|
9
|
+
* Symbol brand identifying a BatsResult instance. Used by matchers to
|
|
10
|
+
* detect BatsResult inputs in a way that survives:
|
|
11
|
+
* - Module-graph duplication (per-entry self-contained bundles produced as
|
|
12
|
+
* the workaround for savvy-web/rslib-builder#158, where each entry has
|
|
13
|
+
* its own copy of the BatsResult class — `instanceof` would fail).
|
|
14
|
+
* - Property name minification (a `constructor.name === "BatsResult"`
|
|
15
|
+
* check would break under any minifier that renames classes).
|
|
16
|
+
*
|
|
17
|
+
* Registered with `Symbol.for(...)` so the symbol is shared across module
|
|
18
|
+
* graphs (the global symbol registry is process-wide).
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
const BATS_RESULT_BRAND = Symbol.for("vitest-bats.BatsResult");
|
|
22
|
+
/**
|
|
23
|
+
* The value returned by `ScriptBuilder.run()`/`.exec()`. Wraps the raw exit status,
|
|
24
|
+
* stdout/stderr (whole and split into lines), and recorded mock calls; assert
|
|
25
|
+
* against it with the `expect.extend` matchers from `vitest-bats/setup`.
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
var BatsResult = class {
|
|
29
|
+
/** @internal */
|
|
30
|
+
[BATS_RESULT_BRAND] = true;
|
|
31
|
+
status;
|
|
32
|
+
output;
|
|
33
|
+
stderr;
|
|
34
|
+
lines;
|
|
35
|
+
stderr_lines;
|
|
36
|
+
calls;
|
|
37
|
+
_jsonCache = null;
|
|
38
|
+
constructor(data) {
|
|
39
|
+
this.status = data.status;
|
|
40
|
+
this.output = data.output;
|
|
41
|
+
this.stderr = data.stderr;
|
|
42
|
+
this.lines = data.output === "" ? [] : data.output.split("\n");
|
|
43
|
+
this.stderr_lines = data.stderr === "" ? [] : data.stderr.split("\n");
|
|
44
|
+
this.calls = data.calls;
|
|
45
|
+
}
|
|
46
|
+
json() {
|
|
47
|
+
if (this._jsonCache) return this._jsonCache.value;
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(this.output);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
53
|
+
throw new Error(`vitest-bats: result.output is not valid JSON: ${msg}`);
|
|
54
|
+
}
|
|
55
|
+
this._jsonCache = { value: parsed };
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const builders = /* @__PURE__ */ new Set();
|
|
60
|
+
/**
|
|
61
|
+
* Resets every live `ScriptBuilder`'s `env()`/`flags()`/`mock()` configuration.
|
|
62
|
+
* Called automatically between tests by `vitest-bats/setup`'s `beforeEach` hook.
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
function resetAllBuilders() {
|
|
66
|
+
for (const b of builders) b.reset();
|
|
67
|
+
}
|
|
68
|
+
function loadDeps() {
|
|
69
|
+
return {
|
|
70
|
+
batsPath: process.env.BATS_PATH ?? "bats",
|
|
71
|
+
batsSupportPath: process.env.BATS_SUPPORT_PATH ?? ""
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function loadKcov(scriptPath) {
|
|
75
|
+
if (process.env.__VITEST_BATS_KCOV__ !== "1") return void 0;
|
|
76
|
+
const cacheDir = process.env.__VITEST_BATS_CACHE_DIR__;
|
|
77
|
+
const kcovPath = process.env.KCOV_PATH;
|
|
78
|
+
if (!cacheDir || !kcovPath) return void 0;
|
|
79
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
80
|
+
const scriptName = scriptPath.split("/").pop()?.replace(/\.sh$/, "") ?? "script";
|
|
81
|
+
const outputDir = resolve(cacheDir, "kcov", `${scriptName}-${id}`);
|
|
82
|
+
mkdirSync(outputDir, { recursive: true });
|
|
83
|
+
return {
|
|
84
|
+
kcovPath,
|
|
85
|
+
outputDir
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function makeRecorderDir() {
|
|
89
|
+
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
90
|
+
const dir = resolve(tmpdir(), `vitest-bats-${process.pid}`, "recorder", id);
|
|
91
|
+
mkdirSync(dir, { recursive: true });
|
|
92
|
+
return dir;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Test-writing API returned by a `.sh` import. Configure with `env()`/`flags()`/
|
|
96
|
+
* `mock()`, then terminate with `run(...args)` or `exec(shellExpr)` to get a
|
|
97
|
+
* {@link BatsResult}.
|
|
98
|
+
* @public
|
|
99
|
+
*/
|
|
100
|
+
var ScriptBuilder = class {
|
|
101
|
+
path;
|
|
102
|
+
name;
|
|
103
|
+
_env = {};
|
|
104
|
+
_flags = "";
|
|
105
|
+
_stubs = [];
|
|
106
|
+
constructor(path, name) {
|
|
107
|
+
this.path = path;
|
|
108
|
+
this.name = name;
|
|
109
|
+
builders.add(this);
|
|
110
|
+
}
|
|
111
|
+
env(vars) {
|
|
112
|
+
this._env = {
|
|
113
|
+
...this._env,
|
|
114
|
+
...vars
|
|
115
|
+
};
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
flags(value) {
|
|
119
|
+
this._flags = value;
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
mock(cmd, responses = {}) {
|
|
123
|
+
this._stubs.push({
|
|
124
|
+
cmd,
|
|
125
|
+
responses
|
|
126
|
+
});
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
run(...args) {
|
|
130
|
+
return this.execute({ kind: "args" }, args);
|
|
131
|
+
}
|
|
132
|
+
exec(shellExpr) {
|
|
133
|
+
return this.execute({
|
|
134
|
+
kind: "shell",
|
|
135
|
+
expression: shellExpr
|
|
136
|
+
}, []);
|
|
137
|
+
}
|
|
138
|
+
reset() {
|
|
139
|
+
this._env = {};
|
|
140
|
+
this._flags = "";
|
|
141
|
+
this._stubs = [];
|
|
142
|
+
}
|
|
143
|
+
execute(mode, args) {
|
|
144
|
+
const recorderDir = makeRecorderDir();
|
|
145
|
+
const deps = loadDeps();
|
|
146
|
+
const kcov = loadKcov(this.path);
|
|
147
|
+
const batsContent = generateBatsFile({
|
|
148
|
+
scriptPath: this.path,
|
|
149
|
+
args,
|
|
150
|
+
env: this._env,
|
|
151
|
+
flags: this._flags,
|
|
152
|
+
stubs: this._stubs,
|
|
153
|
+
recorderDir,
|
|
154
|
+
deps,
|
|
155
|
+
mode,
|
|
156
|
+
...kcov ? { kcov } : {}
|
|
157
|
+
});
|
|
158
|
+
try {
|
|
159
|
+
const data = executeBats({
|
|
160
|
+
batsPath: deps.batsPath,
|
|
161
|
+
batsContent,
|
|
162
|
+
recorderDir
|
|
163
|
+
});
|
|
164
|
+
return new BatsResult(data);
|
|
165
|
+
} finally {
|
|
166
|
+
this.reset();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
//#endregion
|
|
172
|
+
export { BATS_RESULT_BRAND, BatsResult, ScriptBuilder, resetAllBuilders };
|
package/schema.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Ajv } from "ajv";
|
|
2
|
+
import addFormatsModule from "ajv-formats";
|
|
3
|
+
|
|
4
|
+
//#region src/schema.ts
|
|
5
|
+
const addFormats = addFormatsModule.default;
|
|
6
|
+
let ajvSingleton = null;
|
|
7
|
+
function getAjv() {
|
|
8
|
+
if (!ajvSingleton) {
|
|
9
|
+
ajvSingleton = new Ajv({
|
|
10
|
+
strict: false,
|
|
11
|
+
allErrors: true
|
|
12
|
+
});
|
|
13
|
+
addFormats(ajvSingleton);
|
|
14
|
+
}
|
|
15
|
+
return ajvSingleton;
|
|
16
|
+
}
|
|
17
|
+
const compiledCache = /* @__PURE__ */ new WeakMap();
|
|
18
|
+
/**
|
|
19
|
+
* Detects whether `schema` implements the Standard Schema (`~standard`) interface,
|
|
20
|
+
* used by the `toMatchSchema` matcher to route to Standard Schema vs. JSON Schema
|
|
21
|
+
* validation.
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
function isStandardSchema(schema) {
|
|
25
|
+
if (schema === null || typeof schema !== "object") return false;
|
|
26
|
+
if (!("~standard" in schema)) return false;
|
|
27
|
+
const std = schema["~standard"];
|
|
28
|
+
return std !== null && typeof std === "object" && typeof std.validate === "function";
|
|
29
|
+
}
|
|
30
|
+
function pathToString(path) {
|
|
31
|
+
if (!path || path.length === 0) return "(root)";
|
|
32
|
+
return path.map((seg) => {
|
|
33
|
+
const key = typeof seg === "object" && seg !== null && "key" in seg ? seg.key : seg;
|
|
34
|
+
return typeof key === "number" ? `[${key}]` : String(key);
|
|
35
|
+
}).reduce((acc, cur) => {
|
|
36
|
+
if (cur.startsWith("[")) return acc + cur;
|
|
37
|
+
return acc.length === 0 ? cur : `${acc}.${cur}`;
|
|
38
|
+
}, "");
|
|
39
|
+
}
|
|
40
|
+
function validateStandard(schema, value) {
|
|
41
|
+
const result = schema["~standard"].validate(value);
|
|
42
|
+
if (result instanceof Promise) throw new Error("vitest-bats: async Standard Schema validators are not supported. Await result.json() and use a built-in matcher (e.g., await expect(...).resolves.toEqual(...)) instead.");
|
|
43
|
+
if (!result.issues || result.issues.length === 0) return { ok: true };
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
issues: result.issues.map((i) => `${pathToString(i.path)}: ${i.message}`)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function validateJsonSchema(schema, value) {
|
|
50
|
+
let compiled = compiledCache.get(schema);
|
|
51
|
+
if (!compiled) {
|
|
52
|
+
compiled = getAjv().compile(schema);
|
|
53
|
+
compiledCache.set(schema, compiled);
|
|
54
|
+
}
|
|
55
|
+
if (compiled(value)) return { ok: true };
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
issues: (compiled.errors ?? []).map((err) => {
|
|
59
|
+
return `${err.instancePath || "(root)"}: ${err.message ?? "validation failed"}`;
|
|
60
|
+
})
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validates `value` against a Standard Schema validator or a raw JSON Schema
|
|
65
|
+
* object, backing the `toMatchSchema`/`toMatchJsonSchema` matchers.
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
function validate(schema, value) {
|
|
69
|
+
if (isStandardSchema(schema)) return validateStandard(schema, value);
|
|
70
|
+
if (schema !== null && typeof schema === "object") return validateJsonSchema(schema, value);
|
|
71
|
+
throw new Error("vitest-bats: schema must be a Standard Schema validator or a JSON Schema object");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { isStandardSchema, validate };
|
package/setup.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/setup.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resetAllBuilders } from "./runtime.js";
|
|
2
|
+
import { batsMatchers } from "./matchers.js";
|
|
3
|
+
import { beforeEach, expect } from "vitest";
|
|
4
|
+
|
|
5
|
+
//#region src/setup.ts
|
|
6
|
+
expect.extend(batsMatchers);
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
resetAllBuilders();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.59.0"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|