@vitronai/themis 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/benchmark-gate.json +10 -0
- package/bin/themis.js +8 -0
- package/docs/api.md +210 -0
- package/docs/publish.md +55 -0
- package/docs/release-policy.md +54 -0
- package/docs/schemas/agent-result.v1.json +277 -0
- package/docs/schemas/failures.v1.json +78 -0
- package/docs/vscode-extension.md +40 -0
- package/docs/why-themis.md +111 -0
- package/globals.d.ts +22 -0
- package/globals.js +1 -0
- package/index.d.ts +190 -0
- package/index.js +17 -0
- package/package.json +90 -0
- package/src/artifacts.js +207 -0
- package/src/assets/themisBg.png +0 -0
- package/src/assets/themisLogo.png +0 -0
- package/src/assets/themisReport.png +0 -0
- package/src/cli.js +395 -0
- package/src/config.js +52 -0
- package/src/discovery.js +34 -0
- package/src/environment.js +108 -0
- package/src/expect.js +175 -0
- package/src/init.js +22 -0
- package/src/module-loader.js +489 -0
- package/src/reporter.js +2141 -0
- package/src/runner.js +168 -0
- package/src/runtime.js +472 -0
- package/src/snapshots.js +90 -0
- package/src/stability.js +98 -0
- package/src/test-utils.js +201 -0
- package/src/verdict.js +71 -0
- package/src/watch.js +154 -0
- package/src/worker.js +26 -0
package/src/snapshots.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const util = require('util');
|
|
4
|
+
|
|
5
|
+
function createSnapshotState(filePath, options = {}) {
|
|
6
|
+
const snapshotDir = path.join(path.dirname(filePath), '__snapshots__');
|
|
7
|
+
const snapshotFile = path.join(snapshotDir, `${path.basename(filePath)}.snapshots.json`);
|
|
8
|
+
const existing = readSnapshotFile(snapshotFile);
|
|
9
|
+
const nextSnapshots = { ...existing };
|
|
10
|
+
const counters = new Map();
|
|
11
|
+
let dirty = false;
|
|
12
|
+
let currentTestName = null;
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
setCurrentTestName(testName) {
|
|
16
|
+
currentTestName = testName;
|
|
17
|
+
},
|
|
18
|
+
clearCurrentTestName() {
|
|
19
|
+
currentTestName = null;
|
|
20
|
+
},
|
|
21
|
+
matchSnapshot(received, snapshotName) {
|
|
22
|
+
if (!currentTestName) {
|
|
23
|
+
throw new Error('toMatchSnapshot() must be called while a test is running');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const baseKey = snapshotName
|
|
27
|
+
? `${currentTestName}: ${String(snapshotName)}`
|
|
28
|
+
: currentTestName;
|
|
29
|
+
const nextIndex = (counters.get(baseKey) || 0) + 1;
|
|
30
|
+
counters.set(baseKey, nextIndex);
|
|
31
|
+
const snapshotKey = nextIndex === 1 ? baseKey : `${baseKey} (${nextIndex})`;
|
|
32
|
+
const serialized = serializeSnapshot(received);
|
|
33
|
+
|
|
34
|
+
if (!Object.prototype.hasOwnProperty.call(existing, snapshotKey)) {
|
|
35
|
+
nextSnapshots[snapshotKey] = serialized;
|
|
36
|
+
dirty = true;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (existing[snapshotKey] !== serialized) {
|
|
41
|
+
if (options.updateSnapshots) {
|
|
42
|
+
nextSnapshots[snapshotKey] = serialized;
|
|
43
|
+
dirty = true;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Snapshot mismatch for ${snapshotKey}\n\nExpected:\n${existing[snapshotKey]}\n\nReceived:\n${serialized}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
save() {
|
|
53
|
+
if (!dirty) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fs.mkdirSync(snapshotDir, { recursive: true });
|
|
58
|
+
fs.writeFileSync(snapshotFile, `${JSON.stringify(nextSnapshots, null, 2)}\n`, 'utf8');
|
|
59
|
+
return snapshotFile;
|
|
60
|
+
},
|
|
61
|
+
path: snapshotFile
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readSnapshotFile(snapshotFile) {
|
|
66
|
+
if (!fs.existsSync(snapshotFile)) {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return JSON.parse(fs.readFileSync(snapshotFile, 'utf8'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function serializeSnapshot(value) {
|
|
74
|
+
if (typeof value === 'string') {
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return util.inspect(value, {
|
|
79
|
+
depth: 20,
|
|
80
|
+
colors: false,
|
|
81
|
+
sorted: true,
|
|
82
|
+
compact: false,
|
|
83
|
+
breakLength: 80,
|
|
84
|
+
maxArrayLength: null
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
createSnapshotState
|
|
90
|
+
};
|
package/src/stability.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
function buildStabilityReport(runResults) {
|
|
2
|
+
if (!Array.isArray(runResults) || runResults.length === 0) {
|
|
3
|
+
return {
|
|
4
|
+
runs: 0,
|
|
5
|
+
summary: {
|
|
6
|
+
stablePass: 0,
|
|
7
|
+
stableFail: 0,
|
|
8
|
+
unstable: 0
|
|
9
|
+
},
|
|
10
|
+
tests: []
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const runCount = runResults.length;
|
|
15
|
+
const byKey = new Map();
|
|
16
|
+
|
|
17
|
+
for (let runIndex = 0; runIndex < runCount; runIndex += 1) {
|
|
18
|
+
const result = runResults[runIndex] || {};
|
|
19
|
+
const files = Array.isArray(result.files) ? result.files : [];
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
const tests = Array.isArray(file.tests) ? file.tests : [];
|
|
22
|
+
for (const test of tests) {
|
|
23
|
+
if (test.status === 'skipped') {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const key = `${file.file}::${test.fullName}`;
|
|
28
|
+
let entry = byKey.get(key);
|
|
29
|
+
if (!entry) {
|
|
30
|
+
entry = {
|
|
31
|
+
file: file.file,
|
|
32
|
+
testName: test.name,
|
|
33
|
+
fullName: test.fullName,
|
|
34
|
+
statuses: new Array(runCount).fill('missing')
|
|
35
|
+
};
|
|
36
|
+
byKey.set(key, entry);
|
|
37
|
+
}
|
|
38
|
+
entry.statuses[runIndex] = test.status;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const tests = [...byKey.values()].map((entry) => {
|
|
44
|
+
const classification = classifyStability(entry.statuses);
|
|
45
|
+
return {
|
|
46
|
+
file: entry.file,
|
|
47
|
+
testName: entry.testName,
|
|
48
|
+
fullName: entry.fullName,
|
|
49
|
+
statuses: entry.statuses,
|
|
50
|
+
classification
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
tests.sort((a, b) => {
|
|
55
|
+
const fileCompare = a.file.localeCompare(b.file);
|
|
56
|
+
if (fileCompare !== 0) {
|
|
57
|
+
return fileCompare;
|
|
58
|
+
}
|
|
59
|
+
return a.fullName.localeCompare(b.fullName);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const summary = {
|
|
63
|
+
stablePass: tests.filter((entry) => entry.classification === 'stable_pass').length,
|
|
64
|
+
stableFail: tests.filter((entry) => entry.classification === 'stable_fail').length,
|
|
65
|
+
unstable: tests.filter((entry) => entry.classification === 'unstable').length
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
runs: runCount,
|
|
70
|
+
summary,
|
|
71
|
+
tests
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function classifyStability(statuses) {
|
|
76
|
+
if (!Array.isArray(statuses) || statuses.length === 0) {
|
|
77
|
+
return 'unstable';
|
|
78
|
+
}
|
|
79
|
+
if (statuses.every((status) => status === 'passed')) {
|
|
80
|
+
return 'stable_pass';
|
|
81
|
+
}
|
|
82
|
+
if (statuses.every((status) => status === 'failed')) {
|
|
83
|
+
return 'stable_fail';
|
|
84
|
+
}
|
|
85
|
+
return 'unstable';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasStabilityBreaches(report) {
|
|
89
|
+
if (!report || !report.summary) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return report.summary.stableFail > 0 || report.summary.unstable > 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
buildStabilityReport,
|
|
97
|
+
hasStabilityBreaches
|
|
98
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const util = require('util');
|
|
3
|
+
|
|
4
|
+
function createTestUtils(options = {}) {
|
|
5
|
+
const activeMocks = new Set();
|
|
6
|
+
const activeSpies = new Set();
|
|
7
|
+
const moduleLoader = options.moduleLoader;
|
|
8
|
+
|
|
9
|
+
function fn(implementation) {
|
|
10
|
+
const mockFn = createMockFunction(implementation);
|
|
11
|
+
activeMocks.add(mockFn);
|
|
12
|
+
return mockFn;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function spyOn(target, methodName) {
|
|
16
|
+
if (!target || typeof target !== 'object') {
|
|
17
|
+
throw new Error('spyOn expects an object target');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const original = target[methodName];
|
|
21
|
+
if (typeof original !== 'function') {
|
|
22
|
+
throw new Error(`spyOn expects ${String(methodName)} to be a function`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const spy = fn(function spiedMethod(...args) {
|
|
26
|
+
return original.apply(this, args);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
spy.mockRestore = () => {
|
|
30
|
+
target[methodName] = original;
|
|
31
|
+
activeSpies.delete(spy);
|
|
32
|
+
return spy;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
target[methodName] = spy;
|
|
36
|
+
activeSpies.add(spy);
|
|
37
|
+
return spy;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mock(request, factoryOrExports) {
|
|
41
|
+
if (!moduleLoader) {
|
|
42
|
+
throw new Error('mock(...) is unavailable outside the Themis runtime');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const callerFile = resolveCallerFile();
|
|
46
|
+
const normalizedFactory = factoryOrExports === undefined ? {} : factoryOrExports;
|
|
47
|
+
moduleLoader.registerMock(request, callerFile, normalizedFactory);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function unmock(request) {
|
|
51
|
+
if (!moduleLoader) {
|
|
52
|
+
throw new Error('unmock(...) is unavailable outside the Themis runtime');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const callerFile = resolveCallerFile();
|
|
56
|
+
moduleLoader.unregisterMock(request, callerFile);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function clearAllMocks() {
|
|
60
|
+
for (const mockFn of activeMocks) {
|
|
61
|
+
mockFn.mockClear();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resetAllMocks() {
|
|
66
|
+
for (const mockFn of activeMocks) {
|
|
67
|
+
mockFn.mockReset();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (moduleLoader) {
|
|
71
|
+
moduleLoader.clearModuleMocks();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function restoreAllMocks() {
|
|
76
|
+
for (const spy of [...activeSpies]) {
|
|
77
|
+
if (typeof spy.mockRestore === 'function') {
|
|
78
|
+
spy.mockRestore();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
fn,
|
|
85
|
+
spyOn,
|
|
86
|
+
mock,
|
|
87
|
+
unmock,
|
|
88
|
+
clearAllMocks,
|
|
89
|
+
resetAllMocks,
|
|
90
|
+
restoreAllMocks
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createMockFunction(implementation) {
|
|
95
|
+
const state = {
|
|
96
|
+
calls: [],
|
|
97
|
+
results: [],
|
|
98
|
+
implementation: implementation
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function mockFn(...args) {
|
|
102
|
+
state.calls.push(args);
|
|
103
|
+
try {
|
|
104
|
+
const value = state.implementation ? state.implementation.apply(this, args) : undefined;
|
|
105
|
+
state.results.push({ type: 'return', value });
|
|
106
|
+
return value;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
state.results.push({ type: 'throw', value: error });
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Object.defineProperty(mockFn, '_isThemisMockFunction', {
|
|
114
|
+
configurable: false,
|
|
115
|
+
enumerable: false,
|
|
116
|
+
writable: false,
|
|
117
|
+
value: true
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
mockFn.mock = state;
|
|
121
|
+
mockFn.mockImplementation = (nextImplementation) => {
|
|
122
|
+
state.implementation = nextImplementation;
|
|
123
|
+
return mockFn;
|
|
124
|
+
};
|
|
125
|
+
mockFn.mockReturnValue = (value) => {
|
|
126
|
+
state.implementation = () => value;
|
|
127
|
+
return mockFn;
|
|
128
|
+
};
|
|
129
|
+
mockFn.mockResolvedValue = (value) => {
|
|
130
|
+
state.implementation = () => Promise.resolve(value);
|
|
131
|
+
return mockFn;
|
|
132
|
+
};
|
|
133
|
+
mockFn.mockRejectedValue = (value) => {
|
|
134
|
+
state.implementation = () => Promise.reject(value);
|
|
135
|
+
return mockFn;
|
|
136
|
+
};
|
|
137
|
+
mockFn.mockClear = () => {
|
|
138
|
+
state.calls.length = 0;
|
|
139
|
+
state.results.length = 0;
|
|
140
|
+
return mockFn;
|
|
141
|
+
};
|
|
142
|
+
mockFn.mockReset = () => {
|
|
143
|
+
state.calls.length = 0;
|
|
144
|
+
state.results.length = 0;
|
|
145
|
+
state.implementation = undefined;
|
|
146
|
+
return mockFn;
|
|
147
|
+
};
|
|
148
|
+
mockFn.getMockName = () => 'themis.fn';
|
|
149
|
+
|
|
150
|
+
return mockFn;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isMockFunction(value) {
|
|
154
|
+
return Boolean(value && value._isThemisMockFunction);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatMockCalls(value) {
|
|
158
|
+
if (!isMockFunction(value)) {
|
|
159
|
+
return format(value);
|
|
160
|
+
}
|
|
161
|
+
return format(value.mock.calls);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveCallerFile() {
|
|
165
|
+
const stack = String(new Error().stack || '').split('\n').slice(2);
|
|
166
|
+
for (const line of stack) {
|
|
167
|
+
const match = line.match(/\((.+?):\d+:\d+\)$/) || line.match(/at (.+?):\d+:\d+$/);
|
|
168
|
+
if (!match) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const candidate = match[1];
|
|
173
|
+
if (candidate.startsWith('node:')) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const normalized = candidate.replace(/\\/g, '/');
|
|
178
|
+
if (
|
|
179
|
+
normalized.endsWith('/src/test-utils.js') ||
|
|
180
|
+
normalized.endsWith('/src/runtime.js') ||
|
|
181
|
+
normalized.endsWith('/src/expect.js')
|
|
182
|
+
) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return candidate;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return path.join(process.cwd(), '__themis_unknown_caller__.js');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function format(value) {
|
|
193
|
+
return util.inspect(value, { depth: 5, colors: false, maxArrayLength: 20 });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
createTestUtils,
|
|
198
|
+
createMockFunction,
|
|
199
|
+
isMockFunction,
|
|
200
|
+
formatMockCalls
|
|
201
|
+
};
|
package/src/verdict.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
async function verdictReveal(options = {}) {
|
|
2
|
+
const {
|
|
3
|
+
ok = true,
|
|
4
|
+
stream = process.stdout,
|
|
5
|
+
title = 'VERDICT',
|
|
6
|
+
detail = ok ? 'TRUTH UPHELD' : 'TRUTH VIOLATED',
|
|
7
|
+
delayMs = 120
|
|
8
|
+
} = options;
|
|
9
|
+
|
|
10
|
+
if (!canReveal(stream)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
const frames = ok
|
|
16
|
+
? [
|
|
17
|
+
'⚖️ [= ]',
|
|
18
|
+
'⚖️ [== ]',
|
|
19
|
+
'⚖️ [=== ]',
|
|
20
|
+
'⚖️ [==== ]',
|
|
21
|
+
'⚖️ [=====]'
|
|
22
|
+
]
|
|
23
|
+
: [
|
|
24
|
+
'⚖️ [==== ]',
|
|
25
|
+
'⚖️ [=== ]',
|
|
26
|
+
'⚖️ [== ]',
|
|
27
|
+
'⚖️ [= ]',
|
|
28
|
+
'⚖️ [ ]'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const waitMs = normalizeDelay(delayMs);
|
|
32
|
+
|
|
33
|
+
stream.write('\x1B[?25l');
|
|
34
|
+
try {
|
|
35
|
+
for (const frame of frames) {
|
|
36
|
+
stream.write(`\r${frame} ${title}`);
|
|
37
|
+
await sleep(waitMs);
|
|
38
|
+
}
|
|
39
|
+
stream.write(`\r${ok ? '✔' : '✖'} ${title}: ${detail}\n`);
|
|
40
|
+
} finally {
|
|
41
|
+
stream.write('\x1B[?25h');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function canReveal(stream) {
|
|
46
|
+
if (!stream || typeof stream.write !== 'function') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (!stream.isTTY) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (process.env.NO_COLOR) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (process.env.THEMIS_NO_REVEAL === '1') {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeDelay(value) {
|
|
62
|
+
const parsed = Number(value);
|
|
63
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
64
|
+
return 120;
|
|
65
|
+
}
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
verdictReveal
|
|
71
|
+
};
|
package/src/watch.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const WATCHABLE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.json', '.mjs', '.cjs', '.mts', '.cts']);
|
|
6
|
+
const IGNORED_DIRECTORIES = new Set(['node_modules', '.git', '.themis']);
|
|
7
|
+
|
|
8
|
+
function stripWatchFlags(args) {
|
|
9
|
+
return args.filter((token) => token !== '--watch' && token !== '-w');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function collectWatchSignature(cwd) {
|
|
13
|
+
const entries = [];
|
|
14
|
+
walkWatchTree(path.resolve(cwd), entries);
|
|
15
|
+
return entries.sort();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hasWatchSignatureChanged(previousSignature, nextSignature) {
|
|
19
|
+
if (previousSignature.length !== nextSignature.length) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < previousSignature.length; i += 1) {
|
|
24
|
+
if (previousSignature[i] !== nextSignature[i]) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function runWatchMode(options) {
|
|
33
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
34
|
+
const cliArgs = stripWatchFlags(options.cliArgs || []);
|
|
35
|
+
const cliPath = path.join(__dirname, '..', 'bin', 'themis.js');
|
|
36
|
+
const pollIntervalMs = Number(options.pollIntervalMs) > 0 ? Number(options.pollIntervalMs) : 400;
|
|
37
|
+
let previousSignature = collectWatchSignature(cwd);
|
|
38
|
+
let running = false;
|
|
39
|
+
let stopped = false;
|
|
40
|
+
let intervalId = null;
|
|
41
|
+
let activeChild = null;
|
|
42
|
+
let resolveStop = null;
|
|
43
|
+
const stopPromise = new Promise((resolve) => {
|
|
44
|
+
resolveStop = resolve;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const runOnce = () => new Promise((resolve, reject) => {
|
|
48
|
+
activeChild = spawn(process.execPath, [cliPath, 'test', ...cliArgs], {
|
|
49
|
+
cwd,
|
|
50
|
+
stdio: 'inherit',
|
|
51
|
+
env: process.env
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let settled = false;
|
|
55
|
+
const finish = (handler) => (value) => {
|
|
56
|
+
if (settled) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
settled = true;
|
|
60
|
+
activeChild = null;
|
|
61
|
+
handler(value);
|
|
62
|
+
if (stopped && resolveStop) {
|
|
63
|
+
resolveStop();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
activeChild.once('error', finish(reject));
|
|
68
|
+
activeChild.once('exit', () => {
|
|
69
|
+
finish(resolve)();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const shutdown = () => {
|
|
74
|
+
if (stopped) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
stopped = true;
|
|
79
|
+
if (intervalId) {
|
|
80
|
+
clearInterval(intervalId);
|
|
81
|
+
intervalId = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.removeListener('SIGINT', shutdown);
|
|
85
|
+
process.removeListener('SIGTERM', shutdown);
|
|
86
|
+
|
|
87
|
+
if (activeChild && !activeChild.killed) {
|
|
88
|
+
activeChild.kill('SIGINT');
|
|
89
|
+
} else if (resolveStop) {
|
|
90
|
+
resolveStop();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
process.stdout.write('\nWatch mode stopped.\n');
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
process.once('SIGINT', shutdown);
|
|
97
|
+
process.once('SIGTERM', shutdown);
|
|
98
|
+
|
|
99
|
+
await runOnce();
|
|
100
|
+
process.stdout.write('\nWatching for changes...\n');
|
|
101
|
+
|
|
102
|
+
intervalId = setInterval(async () => {
|
|
103
|
+
if (running || stopped) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const nextSignature = collectWatchSignature(cwd);
|
|
108
|
+
if (!hasWatchSignatureChanged(previousSignature, nextSignature)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
previousSignature = nextSignature;
|
|
113
|
+
running = true;
|
|
114
|
+
process.stdout.write('\nChange detected. Re-running...\n');
|
|
115
|
+
await runOnce();
|
|
116
|
+
process.stdout.write('\nWatching for changes...\n');
|
|
117
|
+
running = false;
|
|
118
|
+
}, pollIntervalMs);
|
|
119
|
+
|
|
120
|
+
await stopPromise;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function walkWatchTree(dir, entries) {
|
|
124
|
+
const dirEntries = fs.readdirSync(dir, { withFileTypes: true });
|
|
125
|
+
for (const entry of dirEntries) {
|
|
126
|
+
if (entry.isDirectory()) {
|
|
127
|
+
if (IGNORED_DIRECTORIES.has(entry.name) || entry.name === '__snapshots__') {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
walkWatchTree(path.join(dir, entry.name), entries);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!entry.isFile()) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const extension = path.extname(entry.name);
|
|
139
|
+
if (!WATCHABLE_EXTENSIONS.has(extension)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const targetPath = path.join(dir, entry.name);
|
|
144
|
+
const stats = fs.statSync(targetPath);
|
|
145
|
+
entries.push(`${targetPath}:${Math.round(stats.mtimeMs)}:${stats.size}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
collectWatchSignature,
|
|
151
|
+
hasWatchSignatureChanged,
|
|
152
|
+
runWatchMode,
|
|
153
|
+
stripWatchFlags
|
|
154
|
+
};
|
package/src/worker.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { parentPort, workerData } = require('worker_threads');
|
|
2
|
+
const { collectAndRun } = require('./runtime');
|
|
3
|
+
|
|
4
|
+
(async () => {
|
|
5
|
+
try {
|
|
6
|
+
const result = await collectAndRun(workerData.file, {
|
|
7
|
+
match: workerData.match,
|
|
8
|
+
allowedFullNames: workerData.allowedFullNames,
|
|
9
|
+
noMemes: workerData.noMemes,
|
|
10
|
+
cwd: workerData.cwd,
|
|
11
|
+
environment: workerData.environment,
|
|
12
|
+
setupFiles: workerData.setupFiles,
|
|
13
|
+
tsconfigPath: workerData.tsconfigPath,
|
|
14
|
+
updateSnapshots: workerData.updateSnapshots
|
|
15
|
+
});
|
|
16
|
+
parentPort.postMessage({ ok: true, result });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
parentPort.postMessage({
|
|
19
|
+
ok: false,
|
|
20
|
+
error: {
|
|
21
|
+
message: String(error?.message || error),
|
|
22
|
+
stack: String(error?.stack || error)
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
})();
|