fchek 1.0.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 +64 -0
- package/bin/fchek.js +107 -0
- package/lib/api.js +110 -0
- package/lib/audit.js +211 -0
- package/lib/bench.js +248 -0
- package/lib/config.js +191 -0
- package/lib/context.js +356 -0
- package/lib/convention.js +526 -0
- package/lib/coverage.js +604 -0
- package/lib/db.js +135 -0
- package/lib/deps-check.js +264 -0
- package/lib/deps.js +374 -0
- package/lib/docker.js +84 -0
- package/lib/doctor.js +149 -0
- package/lib/dom.js +226 -0
- package/lib/fuzz.js +470 -0
- package/lib/git.js +290 -0
- package/lib/goto.js +544 -0
- package/lib/launch.js +182 -0
- package/lib/lint.js +624 -0
- package/lib/new_features.test.js +181 -0
- package/lib/output.js +46 -0
- package/lib/port.js +173 -0
- package/lib/process.js +228 -0
- package/lib/profile.js +453 -0
- package/lib/python.js +41 -0
- package/lib/race.js +186 -0
- package/lib/registry.js +179 -0
- package/lib/repl.js +135 -0
- package/lib/run.js +403 -0
- package/lib/screenshot.js +152 -0
- package/lib/secrets.js +257 -0
- package/lib/state.js +219 -0
- package/lib/test.js +471 -0
- package/lib/vuln.js +253 -0
- package/lib/watch.js +240 -0
- package/lib/winlog.js +123 -0
- package/package.json +27 -0
- package/skills/ACTIVATE.md +274 -0
- package/skills/README.md +163 -0
- package/skills/agent.md +444 -0
- package/skills/api.md +47 -0
- package/skills/bench.md +117 -0
- package/skills/context.md +116 -0
- package/skills/convention.md +143 -0
- package/skills/coverage.md +99 -0
- package/skills/csharp.md +97 -0
- package/skills/db.md +66 -0
- package/skills/deps-check.md +135 -0
- package/skills/deps.md +143 -0
- package/skills/docker.md +61 -0
- package/skills/dom.md +56 -0
- package/skills/fuzz.md +167 -0
- package/skills/goto.md +111 -0
- package/skills/lint.md +123 -0
- package/skills/port.md +57 -0
- package/skills/profile.md +91 -0
- package/skills/race.md +117 -0
- package/skills/repl.md +81 -0
- package/skills/rules.md +318 -0
- package/skills/run.md +135 -0
- package/skills/secrets.md +170 -0
- package/skills/security.md +360 -0
- package/skills/state.md +261 -0
- package/skills/vuln.md +57 -0
- package/skills/windows.md +320 -0
package/lib/test.js
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* test.js — auto-detect test framework and run tests
|
|
5
|
+
*
|
|
6
|
+
* Detects: pytest, jest, vitest, cargo test, go test, dotnet test,
|
|
7
|
+
* mocha, jasmine, rspec, phpunit
|
|
8
|
+
*
|
|
9
|
+
* Returns structured JSON: passed, failed, errors, duration.
|
|
10
|
+
* Agent uses this to verify nothing is broken after every change.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { spawnSync, execSync } = require('child_process');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const { output, ok, fail } = require('./output');
|
|
17
|
+
const { findPython } = require('./python');
|
|
18
|
+
|
|
19
|
+
const HELP = `
|
|
20
|
+
fchek test [dir] [--watch] [--coverage] [--filter=<name>]
|
|
21
|
+
|
|
22
|
+
Auto-detect test framework and run tests. Returns structured results.
|
|
23
|
+
|
|
24
|
+
Supported frameworks (auto-detected):
|
|
25
|
+
Python → pytest (preferred) or unittest
|
|
26
|
+
JS/TS → jest, vitest, mocha (reads package.json test script)
|
|
27
|
+
Rust → cargo test
|
|
28
|
+
Go → go test ./...
|
|
29
|
+
C#/.NET → dotnet test
|
|
30
|
+
C/C++ → CMake + CTest / GoogleTest / Catch2
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--coverage Run with coverage enabled
|
|
34
|
+
--filter=<name> Run only tests matching name
|
|
35
|
+
--timeout=60000 Max time in ms (default: 120000)
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
fchek test
|
|
39
|
+
fchek test src/
|
|
40
|
+
fchek test --filter=auth
|
|
41
|
+
fchek test --coverage
|
|
42
|
+
`.trim();
|
|
43
|
+
|
|
44
|
+
const DEFAULT_TIMEOUT = 120_000;
|
|
45
|
+
|
|
46
|
+
function commandExists(cmd) {
|
|
47
|
+
try {
|
|
48
|
+
execSync(
|
|
49
|
+
process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`,
|
|
50
|
+
{ stdio: 'ignore', timeout: 3000 }
|
|
51
|
+
);
|
|
52
|
+
return true;
|
|
53
|
+
} catch { return false; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Framework detection ─────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function detectFramework(dir) {
|
|
59
|
+
const abs = path.resolve(dir);
|
|
60
|
+
|
|
61
|
+
// .fchekrc override
|
|
62
|
+
try {
|
|
63
|
+
const rc = JSON.parse(fs.readFileSync(path.join(abs, '.fchekrc'), 'utf8'));
|
|
64
|
+
if (rc.test_command) return { type: 'custom', command: rc.test_command, dir: abs };
|
|
65
|
+
} catch {}
|
|
66
|
+
|
|
67
|
+
// C# / .NET
|
|
68
|
+
if (fs.readdirSync(abs).some(f => f.endsWith('.csproj') || f.endsWith('.sln'))) {
|
|
69
|
+
return { type: 'dotnet', dir: abs };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Rust
|
|
73
|
+
if (fs.existsSync(path.join(abs, 'Cargo.toml'))) {
|
|
74
|
+
return { type: 'cargo', dir: abs };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Go
|
|
78
|
+
if (fs.existsSync(path.join(abs, 'go.mod'))) {
|
|
79
|
+
return { type: 'go', dir: abs };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// C/C++ — CMakeLists.txt (CTest/GoogleTest/Catch2)
|
|
83
|
+
const hasCMake = fs.existsSync(path.join(abs, 'CMakeLists.txt'));
|
|
84
|
+
const hasMakefile = fs.existsSync(path.join(abs, 'Makefile')) || fs.existsSync(path.join(abs, 'makefile'));
|
|
85
|
+
const hasCFiles = (() => {
|
|
86
|
+
try { return fs.readdirSync(abs).some(f => f.endsWith('.cpp') || f.endsWith('.c')); } catch { return false; }
|
|
87
|
+
})();
|
|
88
|
+
if (hasCMake || (hasMakefile && hasCFiles)) {
|
|
89
|
+
return { type: 'ctest', dir: abs, hasCMake, hasMakefile };
|
|
90
|
+
}
|
|
91
|
+
const pkgPath = path.join(abs, 'package.json');
|
|
92
|
+
if (fs.existsSync(pkgPath)) {
|
|
93
|
+
try {
|
|
94
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
95
|
+
const testScript = pkg.scripts?.test || '';
|
|
96
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
97
|
+
if (deps.vitest || testScript.includes('vitest')) return { type: 'vitest', dir: abs };
|
|
98
|
+
if (deps.jest || testScript.includes('jest')) return { type: 'jest', dir: abs };
|
|
99
|
+
if (deps.mocha || testScript.includes('mocha')) return { type: 'mocha', dir: abs };
|
|
100
|
+
if (testScript) return { type: 'npm_test', dir: abs };
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Python
|
|
105
|
+
const pyMarkers = ['pytest.ini', 'pyproject.toml', 'setup.cfg', 'conftest.py'];
|
|
106
|
+
if (pyMarkers.some(m => fs.existsSync(path.join(abs, m)))) {
|
|
107
|
+
return { type: 'pytest', dir: abs };
|
|
108
|
+
}
|
|
109
|
+
// Fallback: check for test files
|
|
110
|
+
const hasTestPy = (() => {
|
|
111
|
+
try {
|
|
112
|
+
return fs.readdirSync(abs).some(f => f.startsWith('test_') && f.endsWith('.py'));
|
|
113
|
+
} catch { return false; }
|
|
114
|
+
})();
|
|
115
|
+
if (hasTestPy) return { type: 'pytest', dir: abs };
|
|
116
|
+
|
|
117
|
+
// Walk up
|
|
118
|
+
const parent = path.dirname(abs);
|
|
119
|
+
if (parent !== abs) return detectFramework(parent);
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Result parsers ───────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
function parsePytest(stdout, stderr, status) {
|
|
127
|
+
// pytest writes summary to stderr on some versions, stdout on others
|
|
128
|
+
const combined = (stdout || '') + (stderr || '');
|
|
129
|
+
|
|
130
|
+
const passedM = combined.match(/(\d+) passed/);
|
|
131
|
+
const failedM = combined.match(/(\d+) failed/);
|
|
132
|
+
const errorsM = combined.match(/(\d+) error/);
|
|
133
|
+
const skippedM = combined.match(/(\d+) skipped/);
|
|
134
|
+
|
|
135
|
+
const passed = passedM ? parseInt(passedM[1]) : 0;
|
|
136
|
+
const failed = failedM ? parseInt(failedM[1]) : 0;
|
|
137
|
+
const errors = errorsM ? parseInt(errorsM[1]) : 0;
|
|
138
|
+
const skipped = skippedM ? parseInt(skippedM[1]) : 0;
|
|
139
|
+
|
|
140
|
+
// If exit_code 0 and no numbers found — pytest ran but output format unexpected
|
|
141
|
+
// Try alternate: count "PASSED" / "FAILED" lines
|
|
142
|
+
const passedAlt = (combined.match(/\bPASSED\b/g) || []).length;
|
|
143
|
+
const failedAlt = (combined.match(/\bFAILED\b/g) || []).length;
|
|
144
|
+
|
|
145
|
+
const finalPassed = passed > 0 ? passed : passedAlt;
|
|
146
|
+
const finalFailed = failed > 0 ? failed : failedAlt;
|
|
147
|
+
|
|
148
|
+
const failures = [];
|
|
149
|
+
for (const m of combined.matchAll(/FAILED (.+?) - (.+)/g)) {
|
|
150
|
+
failures.push({ test: m[1].trim(), reason: m[2].trim() });
|
|
151
|
+
}
|
|
152
|
+
// Also catch short failure format
|
|
153
|
+
for (const m of combined.matchAll(/FAILED (.+)\n/g)) {
|
|
154
|
+
if (!failures.find(f => f.test === m[1].trim())) {
|
|
155
|
+
failures.push({ test: m[1].trim(), reason: '' });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
passed: finalPassed,
|
|
161
|
+
failed: finalFailed,
|
|
162
|
+
errors,
|
|
163
|
+
skipped,
|
|
164
|
+
failures: failures.slice(0, 20),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseJest(stdout, stderr, status) {
|
|
169
|
+
const combined = stdout + stderr;
|
|
170
|
+
const suitesPassed = parseInt((combined.match(/(\d+) passed/) || [0, 0])[1]);
|
|
171
|
+
const suitesFailed = parseInt((combined.match(/(\d+) failed/) || [0, 0])[1]);
|
|
172
|
+
const tests = parseInt((combined.match(/Tests:\s+.*?(\d+) total/) || [0, 0])[1]);
|
|
173
|
+
|
|
174
|
+
const failures = [];
|
|
175
|
+
for (const m of combined.matchAll(/● (.+)\n\n([\s\S]+?)(?=●|\n\n\n)/g)) {
|
|
176
|
+
failures.push({ test: m[1].trim(), reason: m[2].trim().slice(0, 200) });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { passed: suitesPassed, failed: suitesFailed, total_tests: tests, failures: failures.slice(0, 20) };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseCargo(stdout, stderr, status) {
|
|
183
|
+
const combined = stdout + stderr;
|
|
184
|
+
const passed = parseInt((combined.match(/(\d+) passed/) || [0, 0])[1]);
|
|
185
|
+
const failed = parseInt((combined.match(/(\d+) failed/) || [0, 0])[1]);
|
|
186
|
+
|
|
187
|
+
const failures = [];
|
|
188
|
+
for (const m of combined.matchAll(/---- (.+) stdout ----\n([\s\S]+?)(?=----|\z)/g)) {
|
|
189
|
+
failures.push({ test: m[1].trim(), output: m[2].trim().slice(0, 200) });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { passed, failed, failures: failures.slice(0, 20) };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parseGo(stdout, stderr, status) {
|
|
196
|
+
const combined = stdout + stderr;
|
|
197
|
+
const passed = (combined.match(/--- PASS/g) || []).length;
|
|
198
|
+
const failed = (combined.match(/--- FAIL/g) || []).length;
|
|
199
|
+
|
|
200
|
+
const failures = [];
|
|
201
|
+
for (const m of combined.matchAll(/--- FAIL: (\S+)/g)) {
|
|
202
|
+
failures.push({ test: m[1] });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { passed, failed, failures };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseCtest(stdout, stderr, status) {
|
|
209
|
+
const combined = stdout + stderr;
|
|
210
|
+
|
|
211
|
+
// CTest format: "X/Y Test #N: name ... Passed/Failed Xs"
|
|
212
|
+
const passed = (combined.match(/\bPassed\b/g) || []).length;
|
|
213
|
+
const failed = (combined.match(/\bFailed\b/g) || []).length;
|
|
214
|
+
|
|
215
|
+
// GoogleTest format: "[ PASSED ] N tests." / "[ FAILED ] N tests"
|
|
216
|
+
const gtPassM = combined.match(/\[\s+PASSED\s+\]\s+(\d+) test/);
|
|
217
|
+
const gtFailM = combined.match(/\[\s+FAILED\s+\]\s+(\d+) test/);
|
|
218
|
+
const gtPassed = gtPassM ? parseInt(gtPassM[1]) : null;
|
|
219
|
+
const gtFailed = gtFailM ? parseInt(gtFailM[1]) : null;
|
|
220
|
+
|
|
221
|
+
// Catch2 format: "N test cases, N assertions, N failures"
|
|
222
|
+
const c2M = combined.match(/(\d+) test cases?,\s*(\d+) assertions?,\s*(\d+) failures?/);
|
|
223
|
+
const c2Passed = c2M ? parseInt(c2M[1]) - parseInt(c2M[3]) : null;
|
|
224
|
+
const c2Failed = c2M ? parseInt(c2M[3]) : null;
|
|
225
|
+
|
|
226
|
+
const finalPassed = gtPassed ?? c2Passed ?? passed;
|
|
227
|
+
const finalFailed = gtFailed ?? c2Failed ?? failed;
|
|
228
|
+
|
|
229
|
+
// Collect failed test names from GTest
|
|
230
|
+
const failures = [];
|
|
231
|
+
for (const m of combined.matchAll(/\[\s+FAILED\s+\]\s+(.+)/g)) {
|
|
232
|
+
failures.push({ test: m[1].trim() });
|
|
233
|
+
}
|
|
234
|
+
// CTest failures
|
|
235
|
+
for (const m of combined.matchAll(/Test\s+#\d+:\s+(\S+)\s+.*?Failed/g)) {
|
|
236
|
+
if (!failures.find(f => f.test === m[1])) {
|
|
237
|
+
failures.push({ test: m[1].trim() });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
passed: finalPassed,
|
|
243
|
+
failed: finalFailed,
|
|
244
|
+
failures: failures.slice(0, 20),
|
|
245
|
+
framework_hint: gtPassed !== null ? 'googletest' : c2Passed !== null ? 'catch2' : 'ctest',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function parseDotnet(stdout, stderr, status) {
|
|
250
|
+
const combined = stdout + stderr;
|
|
251
|
+
const passed = parseInt((combined.match(/(\d+) test(?:s)? passed/) || [0, 0])[1]);
|
|
252
|
+
const failed = parseInt((combined.match(/(\d+) test(?:s)? failed/) || [0, 0])[1]);
|
|
253
|
+
const skipped = parseInt((combined.match(/(\d+) test(?:s)? skipped/) || [0, 0])[1]);
|
|
254
|
+
|
|
255
|
+
const failures = [];
|
|
256
|
+
for (const m of combined.matchAll(/X (.+) \[/g)) {
|
|
257
|
+
failures.push({ test: m[1].trim() });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { passed, failed, skipped, failures: failures.slice(0, 20) };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ─── Runners ──────────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
function runFramework(framework, opts, timeoutMs) {
|
|
266
|
+
const { type, dir, command } = framework;
|
|
267
|
+
const { coverage, filter } = opts;
|
|
268
|
+
|
|
269
|
+
let cmd, args, env = { ...process.env };
|
|
270
|
+
|
|
271
|
+
switch (type) {
|
|
272
|
+
case 'custom': {
|
|
273
|
+
const parts = command.split(/\s+/);
|
|
274
|
+
cmd = parts[0]; args = parts.slice(1);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case 'pytest': {
|
|
278
|
+
const py = findPython();
|
|
279
|
+
if (!py) return { error: 'Python not found' };
|
|
280
|
+
// Check if pytest is available
|
|
281
|
+
const checkPytest = spawnSync(py, ['-m', 'pytest', '--version'], {
|
|
282
|
+
encoding: 'utf8', timeout: 5000, windowsHide: true,
|
|
283
|
+
});
|
|
284
|
+
if (checkPytest.status !== 0) {
|
|
285
|
+
return { error: 'pytest not found. Install: pip install pytest' };
|
|
286
|
+
}
|
|
287
|
+
cmd = py;
|
|
288
|
+
args = ['-m', 'pytest', '--tb=short', '-q'];
|
|
289
|
+
if (coverage) args.push('--cov=.', '--cov-report=term-missing');
|
|
290
|
+
if (filter) args.push('-k', filter);
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case 'jest': {
|
|
294
|
+
cmd = commandExists('jest') ? 'jest' : (commandExists('npx') ? 'npx' : null);
|
|
295
|
+
args = cmd === 'npx' ? ['jest'] : [];
|
|
296
|
+
args.push('--json', '--no-coverage');
|
|
297
|
+
if (coverage) args.push('--coverage');
|
|
298
|
+
if (filter) args.push('--testNamePattern', filter);
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case 'vitest': {
|
|
302
|
+
cmd = 'npx';
|
|
303
|
+
args = ['vitest', 'run', '--reporter=json'];
|
|
304
|
+
if (filter) args.push('--testNamePattern', filter);
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case 'mocha': {
|
|
308
|
+
cmd = commandExists('mocha') ? 'mocha' : 'npx';
|
|
309
|
+
args = cmd === 'npx' ? ['mocha'] : [];
|
|
310
|
+
if (filter) args.push('--grep', filter);
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case 'npm_test': {
|
|
314
|
+
cmd = 'npm'; args = ['test'];
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
case 'cargo': {
|
|
318
|
+
cmd = 'cargo'; args = ['test'];
|
|
319
|
+
if (filter) args.push(filter);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
case 'go': {
|
|
323
|
+
cmd = 'go'; args = ['test', './...', '-v'];
|
|
324
|
+
if (filter) args.push('-run', filter);
|
|
325
|
+
if (coverage) args.push('-cover');
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
case 'dotnet': {
|
|
329
|
+
cmd = 'dotnet'; args = ['test', '--logger', 'console;verbosity=normal'];
|
|
330
|
+
if (filter) args.push('--filter', filter);
|
|
331
|
+
if (coverage) args.push('--collect', 'XPlat Code Coverage');
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case 'ctest': {
|
|
335
|
+
// Step 1: Build with CMake or Make
|
|
336
|
+
if (framework.hasCMake) {
|
|
337
|
+
// cmake -B build && cmake --build build
|
|
338
|
+
const cmakeConf = spawnSync(
|
|
339
|
+
'cmake', ['-B', 'build', '-DCMAKE_BUILD_TYPE=Debug'],
|
|
340
|
+
{ encoding: 'utf8', cwd: framework.dir, timeout: 60000, windowsHide: true }
|
|
341
|
+
);
|
|
342
|
+
if (cmakeConf.status !== 0) {
|
|
343
|
+
return { error: `CMake configure failed:\n${(cmakeConf.stderr || cmakeConf.stdout || '').slice(0, 500)}` };
|
|
344
|
+
}
|
|
345
|
+
const cmakeBuild = spawnSync(
|
|
346
|
+
'cmake', ['--build', 'build'],
|
|
347
|
+
{ encoding: 'utf8', cwd: framework.dir, timeout: timeoutMs, windowsHide: true }
|
|
348
|
+
);
|
|
349
|
+
if (cmakeBuild.status !== 0) {
|
|
350
|
+
return { error: `CMake build failed:\n${(cmakeBuild.stderr || cmakeBuild.stdout || '').slice(0, 500)}` };
|
|
351
|
+
}
|
|
352
|
+
// Run with ctest
|
|
353
|
+
const ctestArgs = ['--output-on-failure', '-V'];
|
|
354
|
+
if (filter) ctestArgs.push('-R', filter);
|
|
355
|
+
cmd = 'ctest'; args = ctestArgs;
|
|
356
|
+
} else if (framework.hasMakefile) {
|
|
357
|
+
// make test or make check
|
|
358
|
+
const makeTest = spawnSync(
|
|
359
|
+
'make', ['test'],
|
|
360
|
+
{ encoding: 'utf8', cwd: framework.dir, timeout: timeoutMs, windowsHide: true }
|
|
361
|
+
);
|
|
362
|
+
if (makeTest.status !== 0) {
|
|
363
|
+
// Fallback: try "make check"
|
|
364
|
+
const makeCheck = spawnSync(
|
|
365
|
+
'make', ['check'],
|
|
366
|
+
{ encoding: 'utf8', cwd: framework.dir, timeout: timeoutMs, windowsHide: true }
|
|
367
|
+
);
|
|
368
|
+
return {
|
|
369
|
+
exit_code: makeCheck.status,
|
|
370
|
+
all_passed: makeCheck.status === 0,
|
|
371
|
+
duration_ms: 0,
|
|
372
|
+
stdout: (makeCheck.stdout || '').slice(0, 5000),
|
|
373
|
+
stderr: (makeCheck.stderr || '').slice(0, 2000),
|
|
374
|
+
...parseCtest(makeCheck.stdout || '', makeCheck.stderr || '', makeCheck.status),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
exit_code: makeTest.status,
|
|
379
|
+
all_passed: makeTest.status === 0,
|
|
380
|
+
duration_ms: 0,
|
|
381
|
+
stdout: (makeTest.stdout || '').slice(0, 5000),
|
|
382
|
+
stderr: (makeTest.stderr || '').slice(0, 2000),
|
|
383
|
+
...parseCtest(makeTest.stdout || '', makeTest.stderr || '', makeTest.status),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
default:
|
|
389
|
+
return { error: `Unknown framework: ${type}` };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!cmd) return { error: `Test runner not found for ${type}` };
|
|
393
|
+
|
|
394
|
+
const t0 = Date.now();
|
|
395
|
+
const res = spawnSync(cmd, args, {
|
|
396
|
+
encoding: 'utf8',
|
|
397
|
+
cwd: dir,
|
|
398
|
+
timeout: timeoutMs,
|
|
399
|
+
env,
|
|
400
|
+
windowsHide: true,
|
|
401
|
+
});
|
|
402
|
+
const duration = Date.now() - t0;
|
|
403
|
+
|
|
404
|
+
if (res.error?.code === 'ETIMEDOUT') {
|
|
405
|
+
return { error: `Tests timed out after ${timeoutMs}ms` };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const stdout = res.stdout || '';
|
|
409
|
+
const stderr = res.stderr || '';
|
|
410
|
+
|
|
411
|
+
let parsed;
|
|
412
|
+
switch (type) {
|
|
413
|
+
case 'pytest': parsed = parsePytest(stdout, stderr, res.status); break;
|
|
414
|
+
case 'jest': parsed = parseJest(stdout, stderr, res.status); break;
|
|
415
|
+
case 'vitest': parsed = parseJest(stdout, stderr, res.status); break;
|
|
416
|
+
case 'cargo': parsed = parseCargo(stdout, stderr, res.status); break;
|
|
417
|
+
case 'go': parsed = parseGo(stdout, stderr, res.status); break;
|
|
418
|
+
case 'dotnet': parsed = parseDotnet(stdout, stderr, res.status); break;
|
|
419
|
+
case 'ctest': parsed = parseCtest(stdout, stderr, res.status); break;
|
|
420
|
+
default: parsed = { raw: true };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
...parsed,
|
|
425
|
+
exit_code: res.status,
|
|
426
|
+
all_passed: res.status === 0,
|
|
427
|
+
duration_ms: duration,
|
|
428
|
+
stdout: stdout.slice(0, 5000),
|
|
429
|
+
stderr: stderr.slice(0, 2000),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function run(args) {
|
|
434
|
+
if (args[0] === '--help') { console.log(HELP); return; }
|
|
435
|
+
|
|
436
|
+
const dir = args.find(a => !a.startsWith('--')) || '.';
|
|
437
|
+
const coverage = args.includes('--coverage');
|
|
438
|
+
const filter = (args.find(a => a.startsWith('--filter=')) || '').replace('--filter=', '') || null;
|
|
439
|
+
const timeout = parseInt(
|
|
440
|
+
(args.find(a => a.startsWith('--timeout=')) || `--timeout=${DEFAULT_TIMEOUT}`)
|
|
441
|
+
.replace('--timeout=', ''), 10
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
if (!fs.existsSync(dir)) {
|
|
445
|
+
return output(fail(`Directory not found: ${dir}`));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const framework = detectFramework(path.resolve(dir));
|
|
449
|
+
if (!framework) {
|
|
450
|
+
return output(fail(
|
|
451
|
+
`No test framework detected in: ${path.resolve(dir)}\n` +
|
|
452
|
+
`Supported: pytest, jest, vitest, mocha, cargo test, go test, dotnet test\n` +
|
|
453
|
+
`Or set "test_command" in .fchekrc`
|
|
454
|
+
));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const result = runFramework(framework, { coverage, filter }, timeout);
|
|
458
|
+
|
|
459
|
+
if (result.error) {
|
|
460
|
+
return output(fail(result.error));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
output(ok({
|
|
464
|
+
dir: path.resolve(dir),
|
|
465
|
+
framework: framework.type,
|
|
466
|
+
...result,
|
|
467
|
+
verdict: result.all_passed ? 'all_passed' : 'failures_found',
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
module.exports = { run, detectFramework };
|