project-scan 1.0.0 → 1.0.3
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/bin/project-scan.js +32 -12
- package/lib/html-report.js +47 -9
- package/lib/md-report.js +24 -3
- package/lib/test-runner.js +70 -27
- package/package.json +1 -1
package/bin/project-scan.js
CHANGED
|
@@ -228,20 +228,40 @@ function printTestResults(results) {
|
|
|
228
228
|
const ms = r.duration ? `${c.dim} (${(r.duration/1000).toFixed(2)}s)${c.reset}` : '';
|
|
229
229
|
log(`\n ${icon}${c.reset} ${c.bold}${r.suite}${c.reset}${ms}`);
|
|
230
230
|
|
|
231
|
-
if (r.
|
|
232
|
-
if (r.
|
|
233
|
-
if (r.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (r.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (lines.length > 0) {
|
|
242
|
-
log(` ${c.dim}${c.red}${lines.slice(0, 5).join('\n ')}${c.reset}`);
|
|
231
|
+
if (r.suites) { log(` ${c.dim}${r.suites}${c.reset}`); }
|
|
232
|
+
if (r.status === 'skipped' && r.output) { warn(r.output); }
|
|
233
|
+
if (r.status === 'error' && r.error) { fail(r.error.slice(0, 200)); }
|
|
234
|
+
|
|
235
|
+
// ── Per-test-case LeetCode style list ────────────────────────────────────
|
|
236
|
+
if (r.testCases && r.testCases.length > 0) {
|
|
237
|
+
// Group by file
|
|
238
|
+
const byFile = {};
|
|
239
|
+
for (const tc of r.testCases) {
|
|
240
|
+
(byFile[tc.file] = byFile[tc.file] || []).push(tc);
|
|
243
241
|
}
|
|
242
|
+
for (const [file, cases] of Object.entries(byFile)) {
|
|
243
|
+
log(`\n ${c.dim}📄 ${file}${c.reset}`);
|
|
244
|
+
for (const tc of cases) {
|
|
245
|
+
const tcIcon = tc.status === 'passed' ? `${c.green} ✔` : tc.status === 'failed' ? `${c.red} ✖` : `${c.dim} ○`;
|
|
246
|
+
const tcMs = tc.duration > 0 ? `${c.dim} ${tc.duration}ms${c.reset}` : '';
|
|
247
|
+
log(` ${tcIcon}${c.reset} ${tc.name}${tcMs}`);
|
|
248
|
+
if (tc.error) {
|
|
249
|
+
// First meaningful error line
|
|
250
|
+
const errLine = tc.error.split('\n').find(l => l.trim().startsWith('expect') || l.trim().startsWith('Error') || l.includes('Expected') || l.includes('Received'));
|
|
251
|
+
if (errLine) log(` ${c.red}${c.dim}${errLine.trim()}${c.reset}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
// Fallback summary for non-Jest runners
|
|
257
|
+
if (r.passed > 0) log(` ${c.green}✔ ${r.passed} passed${c.reset}`);
|
|
258
|
+
if (r.failed > 0) log(` ${c.red}✖ ${r.failed} failed${c.reset}`);
|
|
259
|
+
if (r.skipped > 0) log(` ${c.dim}○ ${r.skipped} skipped${c.reset}`);
|
|
244
260
|
}
|
|
261
|
+
|
|
262
|
+
totalPassed += r.passed || 0;
|
|
263
|
+
totalFailed += r.failed || 0;
|
|
264
|
+
totalSkipped += r.skipped || 0;
|
|
245
265
|
}
|
|
246
266
|
|
|
247
267
|
log(`\n ${c.bold}Total: ${c.green}${totalPassed} passed${c.reset} · ${c.red}${totalFailed} failed${c.reset} · ${c.dim}${totalSkipped} skipped${c.reset}`);
|
package/lib/html-report.js
CHANGED
|
@@ -24,28 +24,66 @@ function generateHTML(scanResult, testResults) {
|
|
|
24
24
|
const card = (content, extra = '') =>
|
|
25
25
|
`<div style="background:#f7f8fa;border:1px solid #e5e7eb;border-radius:8px;padding:16px 18px;margin-bottom:10px${extra}">${content}</div>`;
|
|
26
26
|
|
|
27
|
-
// Test suite rows
|
|
27
|
+
// Test suite rows — LeetCode style per-test breakdown
|
|
28
28
|
const testRows = testResults.map(r => {
|
|
29
29
|
const icon = r.status === 'passed' ? '✔' : r.status === 'failed' ? '✖' : r.status === 'error' ? '⚠' : '○';
|
|
30
30
|
const color = r.status === 'passed' ? '#22c55e' : r.status === 'failed' ? '#ef4444' : r.status === 'error' ? '#f59e0b' : '#94a3b8';
|
|
31
31
|
const ms = r.duration ? `${(r.duration / 1000).toFixed(2)}s` : '—';
|
|
32
|
-
const output = r.output ? `<pre style="margin:8px 0 0;padding:10px;background:#1e1e2e;color:#cdd6f4;border-radius:5px;font-size:11px;overflow-x:auto;white-space:pre-wrap;max-height:220px;overflow-y:auto">${esc(r.output.slice(-2500))}</pre>` : '';
|
|
33
|
-
const errOut = r.error ? `<pre style="margin:8px 0 0;padding:10px;background:#2a0a0a;color:#fca5a5;border-radius:5px;font-size:11px;overflow-x:auto;white-space:pre-wrap">${esc(r.error)}</pre>` : '';
|
|
34
32
|
const suitesMeta = r.suites ? `<span style="margin-left:8px;color:#57606a;font-size:12px">(${r.suites})</span>` : '';
|
|
33
|
+
const errOut = r.error ? `<pre style="margin:8px 0 0;padding:10px;background:#2a0a0a;color:#fca5a5;border-radius:5px;font-size:11px;white-space:pre-wrap">${esc(r.error)}</pre>` : '';
|
|
34
|
+
|
|
35
|
+
// Per-test-case LeetCode grid
|
|
36
|
+
let caseGrid = '';
|
|
37
|
+
if (r.testCases && r.testCases.length > 0) {
|
|
38
|
+
// Group by file
|
|
39
|
+
const byFile = {};
|
|
40
|
+
for (const tc of r.testCases) {
|
|
41
|
+
(byFile[tc.file] = byFile[tc.file] || []).push(tc);
|
|
42
|
+
}
|
|
43
|
+
const fileBlocks = Object.entries(byFile).map(([file, cases]) => {
|
|
44
|
+
const rows = cases.map(tc => {
|
|
45
|
+
const tcColor = tc.status === 'passed' ? '#22c55e' : tc.status === 'failed' ? '#ef4444' : '#94a3b8';
|
|
46
|
+
const tcBg = tc.status === 'passed' ? '#f0fdf4' : tc.status === 'failed' ? '#fef2f2' : '#f8fafc';
|
|
47
|
+
const tcBorder = tc.status === 'passed' ? '#bbf7d0' : tc.status === 'failed' ? '#fecaca' : '#e2e8f0';
|
|
48
|
+
const tcIcon = tc.status === 'passed' ? '✔' : tc.status === 'failed' ? '✖' : '○';
|
|
49
|
+
const tcMs = tc.duration > 0 ? `<span style="color:#94a3b8;font-size:11px;margin-left:auto;padding-left:8px;white-space:nowrap">${tc.duration}ms</span>` : '';
|
|
50
|
+
const errBlock = tc.error ? `<pre style="margin:6px 0 0;padding:8px 10px;background:#1e1e2e;color:#fca5a5;border-radius:4px;font-size:10.5px;white-space:pre-wrap;overflow-x:auto">${esc(tc.error)}</pre>` : '';
|
|
51
|
+
return `
|
|
52
|
+
<div style="display:flex;flex-direction:column;border:1px solid ${tcBorder};border-radius:6px;padding:8px 12px;background:${tcBg}">
|
|
53
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
54
|
+
<span style="color:${tcColor};font-size:14px;font-weight:700;flex-shrink:0">${tcIcon}</span>
|
|
55
|
+
<span style="font-size:12.5px;color:#1f2328;flex:1;line-height:1.4">${esc(tc.name)}</span>
|
|
56
|
+
${tcMs}
|
|
57
|
+
</div>
|
|
58
|
+
${errBlock}
|
|
59
|
+
</div>`;
|
|
60
|
+
}).join('');
|
|
61
|
+
return `
|
|
62
|
+
<div style="margin-bottom:12px">
|
|
63
|
+
<div style="font-size:11px;color:#57606a;font-family:monospace;margin-bottom:6px;padding:3px 8px;background:#f0f3f6;border-radius:4px;display:inline-block">${esc(file)}</div>
|
|
64
|
+
<div style="display:flex;flex-direction:column;gap:5px">${rows}</div>
|
|
65
|
+
</div>`;
|
|
66
|
+
}).join('');
|
|
67
|
+
caseGrid = `<div style="margin-top:12px">${fileBlocks}</div>`;
|
|
68
|
+
} else if (r.output && r.status !== 'skipped') {
|
|
69
|
+
// Fallback: raw output for non-Jest runners
|
|
70
|
+
caseGrid = `<pre style="margin:8px 0 0;padding:10px;background:#1e1e2e;color:#cdd6f4;border-radius:5px;font-size:11px;overflow-x:auto;white-space:pre-wrap;max-height:200px;overflow-y:auto">${esc(r.output.slice(-2000))}</pre>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
35
73
|
return `
|
|
36
|
-
<div style="border:1px solid #e5e7eb;border-radius:
|
|
74
|
+
<div style="border:1px solid #e5e7eb;border-radius:8px;padding:14px 16px;margin-bottom:12px;background:#fff">
|
|
37
75
|
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:4px">
|
|
38
|
-
<span style="font-weight:
|
|
76
|
+
<span style="font-weight:700;color:#1f2328;font-size:14px">
|
|
39
77
|
<span style="color:${color};margin-right:6px;font-size:16px">${icon}</span>${esc(r.suite)}${suitesMeta}
|
|
40
78
|
</span>
|
|
41
79
|
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
|
|
42
|
-
${r.passed
|
|
43
|
-
${r.failed
|
|
44
|
-
${r.skipped
|
|
80
|
+
${r.passed > 0 ? `<span style="color:#22c55e;font-size:12px;font-weight:700">${r.passed} passed</span>` : ''}
|
|
81
|
+
${r.failed > 0 ? `<span style="color:#ef4444;font-size:12px;font-weight:700">${r.failed} failed</span>` : ''}
|
|
82
|
+
${r.skipped > 0 ? `<span style="color:#94a3b8;font-size:12px;font-weight:700">${r.skipped} skipped</span>` : ''}
|
|
45
83
|
<span style="color:#57606a;font-size:12px">${ms}</span>
|
|
46
84
|
</div>
|
|
47
85
|
</div>
|
|
48
|
-
${
|
|
86
|
+
${caseGrid}${errOut}
|
|
49
87
|
</div>`;
|
|
50
88
|
}).join('');
|
|
51
89
|
|
package/lib/md-report.js
CHANGED
|
@@ -217,9 +217,30 @@ function generateTestsReport(scanResult, testResults) {
|
|
|
217
217
|
const suiteDetails = testResults.map(r => {
|
|
218
218
|
const icon = r.status === 'passed' ? '✅' : r.status === 'failed' ? '❌' : r.status === 'error' ? '⚠️' : '⏭️';
|
|
219
219
|
const ms = r.duration ? `${(r.duration/1000).toFixed(2)}s` : '—';
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
const error = r.error ? `\n\n> **Error:** ${r.error}` : '';
|
|
221
|
+
|
|
222
|
+
// Per-test-case rows (LeetCode style)
|
|
223
|
+
let caseTable = '';
|
|
224
|
+
if (r.testCases && r.testCases.length > 0) {
|
|
225
|
+
const byFile = {};
|
|
226
|
+
for (const tc of r.testCases) {
|
|
227
|
+
(byFile[tc.file] = byFile[tc.file] || []).push(tc);
|
|
228
|
+
}
|
|
229
|
+
const blocks = Object.entries(byFile).map(([file, cases]) => {
|
|
230
|
+
const rows = cases.map(tc => {
|
|
231
|
+
const tcIcon = tc.status === 'passed' ? '✅' : tc.status === 'failed' ? '❌' : '⏭️';
|
|
232
|
+
const tcMs = tc.duration > 0 ? `${tc.duration}ms` : '—';
|
|
233
|
+
const errLine = tc.error
|
|
234
|
+
? `\n > \`\`\`\n > ${tc.error.split('\n').slice(0,4).join('\n > ')}\n > \`\`\``
|
|
235
|
+
: '';
|
|
236
|
+
return `| ${tcIcon} | ${tc.name} | ${tcMs} |${errLine}`;
|
|
237
|
+
}).join('\n');
|
|
238
|
+
return `**\`${file}\`**\n\n| | Test Case | Duration |\n|--|-----------|----------|\n${rows}`;
|
|
239
|
+
});
|
|
240
|
+
caseTable = '\n\n' + blocks.join('\n\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return `### ${icon} ${r.suite}\n\n| Metric | Value |\n|--------|-------|\n| Status | \`${r.status}\` |\n| Passed | ${r.passed||0} |\n| Failed | ${r.failed||0} |\n| Skipped | ${r.skipped||0} |\n| Duration | ${ms} |\n${r.suites ? `| Suites | ${r.suites} |\n` : ''}${caseTable}${error}`;
|
|
223
244
|
}).join('\n\n---\n\n');
|
|
224
245
|
|
|
225
246
|
const testFilesList = (scanResult.structure.testFiles || []).slice(0, 30).map(f => `- \`${f}\``).join('\n') || '- _None found_';
|
package/lib/test-runner.js
CHANGED
|
@@ -9,6 +9,8 @@ const { execSync, spawnSync } = require('child_process');
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
12
|
+
const IS_WIN = process.platform === 'win32';
|
|
13
|
+
|
|
12
14
|
// ─── Per-language runner configs ─────────────────────────────────────────────
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -100,9 +102,9 @@ const RUNNERS = [
|
|
|
100
102
|
hasSourceExt(root, '.py');
|
|
101
103
|
},
|
|
102
104
|
run(root) {
|
|
103
|
-
const cmd = commandExists('pytest')
|
|
104
|
-
: commandExists('python3')
|
|
105
|
-
: commandExists('python')
|
|
105
|
+
const cmd = commandExists('pytest') ? 'pytest -v --tb=short'
|
|
106
|
+
: commandExists('python3') ? 'python3 -m pytest -v --tb=short'
|
|
107
|
+
: commandExists('python') ? 'python -m pytest -v --tb=short'
|
|
106
108
|
: null;
|
|
107
109
|
return [runCommand({ dir: root, label: '(root)', cmd, suite: 'pytest', parseOutput: parsePytestOutput })];
|
|
108
110
|
}
|
|
@@ -115,7 +117,7 @@ const RUNNERS = [
|
|
|
115
117
|
run(root) {
|
|
116
118
|
return [runCommand({
|
|
117
119
|
dir: root, label: '(root)',
|
|
118
|
-
cmd: commandExists('go') ? 'go test ./... -v
|
|
120
|
+
cmd: commandExists('go') ? 'go test ./... -v' : null,
|
|
119
121
|
suite: 'Go test',
|
|
120
122
|
parseOutput: parseGoTestOutput,
|
|
121
123
|
})];
|
|
@@ -129,7 +131,7 @@ const RUNNERS = [
|
|
|
129
131
|
run(root) {
|
|
130
132
|
return [runCommand({
|
|
131
133
|
dir: root, label: '(root)',
|
|
132
|
-
cmd: commandExists('cargo') ? 'cargo test
|
|
134
|
+
cmd: commandExists('cargo') ? 'cargo test' : null,
|
|
133
135
|
suite: 'Cargo test',
|
|
134
136
|
parseOutput: parseCargoOutput,
|
|
135
137
|
})];
|
|
@@ -141,7 +143,7 @@ const RUNNERS = [
|
|
|
141
143
|
name: 'Maven',
|
|
142
144
|
detect(root) { return fs.existsSync(path.join(root, 'pom.xml')); },
|
|
143
145
|
run(root) {
|
|
144
|
-
const cmd = commandExists('mvn') ? 'mvn test -B
|
|
146
|
+
const cmd = commandExists('mvn') ? 'mvn test -B' : null;
|
|
145
147
|
return [runCommand({ dir: root, label: '(root)', cmd, suite: 'Maven', parseOutput: parseMavenOutput })];
|
|
146
148
|
}
|
|
147
149
|
},
|
|
@@ -154,8 +156,13 @@ const RUNNERS = [
|
|
|
154
156
|
fs.existsSync(path.join(root, 'build.gradle.kts'));
|
|
155
157
|
},
|
|
156
158
|
run(root) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
// Windows uses gradlew.bat; Unix uses ./gradlew
|
|
160
|
+
const wrapperWin = path.join(root, 'gradlew.bat');
|
|
161
|
+
const wrapperUnix = path.join(root, 'gradlew');
|
|
162
|
+
const gradleCmd = fs.existsSync(IS_WIN ? wrapperWin : wrapperUnix)
|
|
163
|
+
? (IS_WIN ? 'gradlew.bat' : './gradlew')
|
|
164
|
+
: (commandExists('gradle') ? 'gradle' : null);
|
|
165
|
+
return [runCommand({ dir: root, label: '(root)', cmd: gradleCmd ? `${gradleCmd} test` : null, suite: 'Gradle', parseOutput: parseGradleOutput })];
|
|
159
166
|
}
|
|
160
167
|
},
|
|
161
168
|
|
|
@@ -163,14 +170,16 @@ const RUNNERS = [
|
|
|
163
170
|
{
|
|
164
171
|
name: 'PHPUnit',
|
|
165
172
|
detect(root) {
|
|
166
|
-
return fs.existsSync(path.join(root, 'phpunit.xml'))
|
|
167
|
-
fs.existsSync(path.join(root, 'phpunit.xml.dist'))
|
|
168
|
-
fs.existsSync(path.join(root, 'vendor
|
|
173
|
+
return fs.existsSync(path.join(root, 'phpunit.xml')) ||
|
|
174
|
+
fs.existsSync(path.join(root, 'phpunit.xml.dist')) ||
|
|
175
|
+
fs.existsSync(path.join(root, 'vendor', 'bin', 'phpunit')) ||
|
|
169
176
|
hasSourceExt(root, '.php');
|
|
170
177
|
},
|
|
171
178
|
run(root) {
|
|
172
|
-
const
|
|
173
|
-
|
|
179
|
+
const vendorBin = path.join(root, 'vendor', 'bin', 'phpunit');
|
|
180
|
+
const cmd = fs.existsSync(vendorBin)
|
|
181
|
+
? `"${vendorBin}" --testdox`
|
|
182
|
+
: commandExists('phpunit') ? 'phpunit --testdox' : null;
|
|
174
183
|
return [runCommand({ dir: root, label: '(root)', cmd, suite: 'PHPUnit', parseOutput: parsePHPUnitOutput })];
|
|
175
184
|
}
|
|
176
185
|
},
|
|
@@ -184,7 +193,7 @@ const RUNNERS = [
|
|
|
184
193
|
fs.existsSync(path.join(root, 'spec'));
|
|
185
194
|
},
|
|
186
195
|
run(root) {
|
|
187
|
-
const cmd = commandExists('rspec') ? 'rspec --format progress
|
|
196
|
+
const cmd = commandExists('rspec') ? 'rspec --format progress' : (commandExists('bundle') ? 'bundle exec rspec --format progress' : null);
|
|
188
197
|
return [runCommand({ dir: root, label: '(root)', cmd, suite: 'RSpec', parseOutput: parseRSpecOutput })];
|
|
189
198
|
}
|
|
190
199
|
},
|
|
@@ -194,7 +203,7 @@ const RUNNERS = [
|
|
|
194
203
|
name: 'Flutter/Dart test',
|
|
195
204
|
detect(root) { return fs.existsSync(path.join(root, 'pubspec.yaml')); },
|
|
196
205
|
run(root) {
|
|
197
|
-
const cmd = commandExists('flutter') ? 'flutter test
|
|
206
|
+
const cmd = commandExists('flutter') ? 'flutter test' : (commandExists('dart') ? 'dart test' : null);
|
|
198
207
|
return [runCommand({ dir: root, label: '(root)', cmd, suite: 'Flutter/Dart', parseOutput: parseDartOutput })];
|
|
199
208
|
}
|
|
200
209
|
},
|
|
@@ -208,7 +217,7 @@ const RUNNERS = [
|
|
|
208
217
|
} catch { return false; }
|
|
209
218
|
},
|
|
210
219
|
run(root) {
|
|
211
|
-
const cmd = commandExists('dotnet') ? 'dotnet test --logger "console;verbosity=normal"
|
|
220
|
+
const cmd = commandExists('dotnet') ? 'dotnet test --logger "console;verbosity=normal"' : null;
|
|
212
221
|
return [runCommand({ dir: root, label: '(root)', cmd, suite: 'dotnet test', parseOutput: parseDotnetOutput })];
|
|
213
222
|
}
|
|
214
223
|
},
|
|
@@ -252,17 +261,18 @@ function runAllTests(rootDir) {
|
|
|
252
261
|
|
|
253
262
|
function runJest(dir, label) {
|
|
254
263
|
const start = Date.now();
|
|
255
|
-
if (!fs.existsSync(path.join(dir, 'node_modules', '.bin', 'jest'))
|
|
264
|
+
if (!fs.existsSync(path.join(dir, 'node_modules', '.bin', 'jest')) &&
|
|
265
|
+
!fs.existsSync(path.join(dir, 'node_modules', '.bin', 'jest.cmd')) &&
|
|
256
266
|
!fs.existsSync(path.join(dir, 'node_modules', 'jest'))) {
|
|
257
|
-
// Try npm test fallback
|
|
258
267
|
const pkg = tryReadJSON(path.join(dir, 'package.json'));
|
|
259
268
|
if (!pkg?.scripts?.test) {
|
|
260
269
|
return { suite: `Jest (${label})`, status: 'skipped', passed: 0, failed: 0, skipped: 0, output: 'Jest not installed. Run npm install first.', duration: 0 };
|
|
261
270
|
}
|
|
262
271
|
}
|
|
263
272
|
|
|
273
|
+
// shell:true is required on Windows so npx.cmd is resolved correctly
|
|
264
274
|
const result = spawnSync('npx', ['jest', '--no-coverage', '--json', '--forceExit'], {
|
|
265
|
-
cwd: dir, encoding: 'utf8', timeout: 120000,
|
|
275
|
+
cwd: dir, encoding: 'utf8', timeout: 120000, shell: true,
|
|
266
276
|
env: { ...process.env, FORCE_COLOR: '0' }
|
|
267
277
|
});
|
|
268
278
|
|
|
@@ -280,17 +290,29 @@ function runJest(dir, label) {
|
|
|
280
290
|
const skipped = jResult.numPendingTests || 0;
|
|
281
291
|
const status = failed > 0 ? 'failed' : (passed > 0 ? 'passed' : 'skipped');
|
|
282
292
|
|
|
283
|
-
// Build
|
|
293
|
+
// Build structured per-test case list (LeetCode style)
|
|
294
|
+
// Jest uses 'assertionResults' per suite (not 'testResults')
|
|
295
|
+
const testCases = [];
|
|
284
296
|
const lines = [];
|
|
285
297
|
for (const suite of (jResult.testResults || [])) {
|
|
286
|
-
const relPath = path.relative(dir, suite.testFilePath);
|
|
298
|
+
const relPath = path.relative(dir, suite.testFilePath || suite.name || '');
|
|
287
299
|
lines.push(`\n 📄 ${relPath}`);
|
|
288
|
-
|
|
300
|
+
const assertions = suite.assertionResults || suite.testResults || [];
|
|
301
|
+
for (const t of assertions) {
|
|
289
302
|
const icon = t.status === 'passed' ? ' ✔' : t.status === 'failed' ? ' ✖' : ' ○';
|
|
290
|
-
lines.push(`${icon} ${t.fullName}`);
|
|
303
|
+
lines.push(`${icon} ${t.fullName || t.title}`);
|
|
291
304
|
if (t.status === 'failed' && t.failureMessages?.length) {
|
|
292
305
|
lines.push(` ${t.failureMessages[0].split('\n').slice(0, 3).join('\n ')}`);
|
|
293
306
|
}
|
|
307
|
+
testCases.push({
|
|
308
|
+
file: relPath,
|
|
309
|
+
name: t.fullName || t.title || '(unnamed)',
|
|
310
|
+
status: t.status, // 'passed' | 'failed' | 'pending'
|
|
311
|
+
duration: t.duration || 0,
|
|
312
|
+
error: t.status === 'failed' && t.failureMessages?.length
|
|
313
|
+
? t.failureMessages[0].split('\n').slice(0, 6).join('\n')
|
|
314
|
+
: null,
|
|
315
|
+
});
|
|
294
316
|
}
|
|
295
317
|
}
|
|
296
318
|
|
|
@@ -301,6 +323,7 @@ function runJest(dir, label) {
|
|
|
301
323
|
duration,
|
|
302
324
|
output: lines.join('\n'),
|
|
303
325
|
suites: jResult.numPassedTestSuites + '/' + jResult.numTotalTestSuites + ' suites passed',
|
|
326
|
+
testCases,
|
|
304
327
|
};
|
|
305
328
|
} catch { /* fall through to text parse */ }
|
|
306
329
|
}
|
|
@@ -316,7 +339,11 @@ function runCommand({ dir, label, cmd, suite, parseOutput }) {
|
|
|
316
339
|
}
|
|
317
340
|
const start = Date.now();
|
|
318
341
|
try {
|
|
319
|
-
|
|
342
|
+
// shell:true is required on Windows; harmless on Unix
|
|
343
|
+
const output = execSync(cmd, {
|
|
344
|
+
cwd: dir, encoding: 'utf8', timeout: 120000, shell: true,
|
|
345
|
+
env: { ...process.env, FORCE_COLOR: '0' }, stdio: 'pipe'
|
|
346
|
+
});
|
|
320
347
|
const duration = Date.now() - start;
|
|
321
348
|
return { suite: `${suite} (${label})`, duration, ...parseOutput(output, true) };
|
|
322
349
|
} catch (err) {
|
|
@@ -419,12 +446,28 @@ function parseDotnetOutput(output, exitOk) {
|
|
|
419
446
|
return { status: exitOk ? 'passed' : 'failed', passed: p ? parseInt(p[1]) : 0, failed: f ? parseInt(f[1]) : 0, skipped: 0, output: output.slice(-3000) };
|
|
420
447
|
}
|
|
421
448
|
|
|
422
|
-
// ─── Utility helpers
|
|
449
|
+
// ─── Utility helpers ─────────────────────────────────────────────────────────
|
|
423
450
|
|
|
424
451
|
function tryReadJSON(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } }
|
|
425
452
|
function allDeps(pkg) { return [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {}), ...Object.keys(pkg.peerDependencies || {})]; }
|
|
426
|
-
|
|
427
|
-
|
|
453
|
+
|
|
454
|
+
/** Works on Windows (where and Get-Command) and Unix (which / command -v). */
|
|
455
|
+
function commandExists(cmd) {
|
|
456
|
+
try {
|
|
457
|
+
if (IS_WIN) {
|
|
458
|
+
execSync(`where ${cmd}`, { encoding: 'utf8', stdio: 'pipe' });
|
|
459
|
+
} else {
|
|
460
|
+
execSync(`command -v ${cmd}`, { encoding: 'utf8', stdio: 'pipe', shell: '/bin/sh' });
|
|
461
|
+
}
|
|
462
|
+
return true;
|
|
463
|
+
} catch { return false; }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function hasBin(dir, bin) {
|
|
467
|
+
// On Windows the bin is a .cmd wrapper
|
|
468
|
+
return fs.existsSync(path.join(dir, 'node_modules', '.bin', bin)) ||
|
|
469
|
+
fs.existsSync(path.join(dir, 'node_modules', '.bin', bin + '.cmd'));
|
|
470
|
+
}
|
|
428
471
|
function hasSourceExt(root, ext) {
|
|
429
472
|
try { return fs.readdirSync(root).some(f => f.endsWith(ext)); } catch { return false; }
|
|
430
473
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-scan",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Universal project summarizer & test runner — scans any project for languages, frameworks, databases, cloud services, CI/CD pipelines and runs tests. Works with JavaScript, TypeScript, Python, Go, Rust, Java, Kotlin, PHP, Ruby, Dart, C#, and more.",
|
|
5
5
|
"main": "lib/scanner.js",
|
|
6
6
|
"bin": {
|