datagrok-tools 6.0.8 → 6.1.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/.devcontainer/entrypoint.sh +0 -0
- package/bin/commands/config.js +42 -6
- package/bin/commands/docker-gen.js +16 -0
- package/bin/commands/help.js +30 -15
- package/bin/commands/publish.js +275 -15
- package/bin/commands/test.js +545 -12
- package/bin/grok.js +2 -0
- package/bin/utils/python-celery-gen.js +247 -0
- package/bin/utils/test-utils.js +39 -17
- package/bin/utils/utils.js +1 -1
- package/config-template.yaml +2 -0
- package/package.json +1 -1
package/bin/commands/test.js
CHANGED
|
@@ -4,12 +4,14 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", {
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
|
+
exports.matchesFilter = matchesFilter;
|
|
7
8
|
exports.test = test;
|
|
8
9
|
var _child_process = require("child_process");
|
|
9
10
|
var _util = require("util");
|
|
10
11
|
var _fs = _interopRequireDefault(require("fs"));
|
|
11
12
|
var _os = _interopRequireDefault(require("os"));
|
|
12
13
|
var _path = _interopRequireDefault(require("path"));
|
|
14
|
+
var _readline = _interopRequireDefault(require("readline"));
|
|
13
15
|
var _jsYaml = _interopRequireDefault(require("js-yaml"));
|
|
14
16
|
var utils = _interopRequireWildcard(require("../utils/utils"));
|
|
15
17
|
var color = _interopRequireWildcard(require("../utils/color-utils"));
|
|
@@ -23,12 +25,85 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
23
25
|
const execAsync = (0, _util.promisify)(_child_process.exec);
|
|
24
26
|
const execFileAsync = (0, _util.promisify)(_child_process.execFile);
|
|
25
27
|
const testInvocationTimeout = 3600000;
|
|
26
|
-
const availableCommandOptions = ['host', 'package', 'csv', 'gui', 'catchUnhandled', 'platform', 'core', 'report', 'skip-build', 'skip-publish', 'path', 'record', 'verbose', 'benchmark', 'category', 'test', 'stress-test', 'link', 'tag', 'ci-cd', 'debug', 'no-retry'];
|
|
28
|
+
const availableCommandOptions = ['host', 'package', 'csv', 'gui', 'catchUnhandled', 'platform', 'core', 'report', 'skip-build', 'skip-publish', 'path', 'record', 'verbose', 'benchmark', 'category', 'test', 'stress-test', 'link', 'tag', 'ci-cd', 'debug', 'no-retry', 'dartium', 'f', 'params', 'logfailed'];
|
|
27
29
|
const curDir = process.cwd();
|
|
30
|
+
|
|
31
|
+
/** Expands camelCase to space-separated lowercase: "dataManipulation" → "data manipulation" */
|
|
32
|
+
function expandCamelCase(s) {
|
|
33
|
+
return s.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Checks if a segment matches a filter segment (case-insensitive substring, with camelCase expansion) */
|
|
37
|
+
function segmentMatches(testSegment, filterSegment) {
|
|
38
|
+
if (filterSegment === '*') return true;
|
|
39
|
+
const t = expandCamelCase(testSegment.trim());
|
|
40
|
+
const f = expandCamelCase(filterSegment.trim());
|
|
41
|
+
return t.includes(f);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fluent test name filter. Matches a full test name like "Core: d4 | Viewers | Data Manipulation | Scatter plot".
|
|
46
|
+
*
|
|
47
|
+
* Supports: substring search, `/`-anchored paths, `|`/`/` delimited segments, camelCase expansion, `*` wildcards.
|
|
48
|
+
*/
|
|
49
|
+
function matchesFilter(fullName, filter) {
|
|
50
|
+
// Normalize test name: strip "Core: " prefix, split by " | "
|
|
51
|
+
const normalized = fullName.replace(/^Core:\s*/, '');
|
|
52
|
+
const testSegments = normalized.split(/\s*\|\s*/);
|
|
53
|
+
|
|
54
|
+
// Detect anchored mode: ^ prefix
|
|
55
|
+
const anchored = filter.startsWith('^');
|
|
56
|
+
const rawFilter = anchored ? filter.slice(1) : filter;
|
|
57
|
+
|
|
58
|
+
// Split filter by / or | (with optional spaces)
|
|
59
|
+
const filterParts = rawFilter.split(/\s*[/|]\s*/).map(s => s.trim()).filter(s => s.length > 0);
|
|
60
|
+
|
|
61
|
+
// Substring mode: single segment, no delimiters, no anchor
|
|
62
|
+
if (filterParts.length <= 1 && !anchored) {
|
|
63
|
+
const f = expandCamelCase(filterParts[0] || '');
|
|
64
|
+
const full = expandCamelCase(normalized);
|
|
65
|
+
return full.includes(f);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Segment mode: find filterParts in order within testSegments
|
|
69
|
+
if (anchored) {
|
|
70
|
+
// Anchored: filter[0] must match testSegments[0], filter[1] must match testSegments[1], etc.
|
|
71
|
+
if (filterParts.length > testSegments.length) return false;
|
|
72
|
+
for (let i = 0; i < filterParts.length; i++) {
|
|
73
|
+
if (!segmentMatches(testSegments[i], filterParts[i])) return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Unanchored: find filter segments in order (with possible gaps, unless wildcard forces position)
|
|
79
|
+
let ti = 0;
|
|
80
|
+
for (let fi = 0; fi < filterParts.length; fi++) {
|
|
81
|
+
let found = false;
|
|
82
|
+
while (ti < testSegments.length) {
|
|
83
|
+
if (segmentMatches(testSegments[ti], filterParts[fi])) {
|
|
84
|
+
ti++;
|
|
85
|
+
found = true;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
// If previous filter was wildcard (exact position), don't skip
|
|
89
|
+
if (fi > 0 && filterParts[fi - 1] === '*') {
|
|
90
|
+
// Wildcard consumed exactly one segment, so this filter must match next
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
ti++;
|
|
94
|
+
}
|
|
95
|
+
if (!found) return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
28
99
|
const grokDir = _path.default.join(_os.default.homedir(), '.grok');
|
|
29
100
|
const confPath = _path.default.join(grokDir, 'config.yaml');
|
|
30
101
|
const consoleLogOutputDir = _path.default.join(curDir, 'test-console-output.log');
|
|
31
|
-
const
|
|
102
|
+
const defaultCsvReportDir = _path.default.join(curDir, 'test-report.csv');
|
|
103
|
+
function resolveCsvPath(csv) {
|
|
104
|
+
if (typeof csv === 'string' && csv !== 'true' && csv.length > 0) return _path.default.resolve(csv);
|
|
105
|
+
return defaultCsvReportDir;
|
|
106
|
+
}
|
|
32
107
|
|
|
33
108
|
/**
|
|
34
109
|
* Detects if the current directory is within a Dart library folder (d4, xamgle, ddt, dml)
|
|
@@ -57,12 +132,31 @@ function findGitRoot(startDir) {
|
|
|
57
132
|
}
|
|
58
133
|
}
|
|
59
134
|
async function test(args) {
|
|
135
|
+
if (args['_'][1] === 'list') return await listTests(args);
|
|
136
|
+
if (args.dartium) return await testDartium(args);
|
|
60
137
|
if (args.recursive) return await testRecursive(process.cwd(), args);
|
|
61
138
|
const config = _jsYaml.default.load(_fs.default.readFileSync(confPath, {
|
|
62
139
|
encoding: 'utf-8'
|
|
63
140
|
}));
|
|
64
141
|
isArgsValid(args);
|
|
65
142
|
|
|
143
|
+
// Resolve fluent filter into category/test for normal mode (best-effort)
|
|
144
|
+
const filter = resolveFilter(args);
|
|
145
|
+
if (filter && !args.category && !args.test) {
|
|
146
|
+
const anchored = filter.startsWith('/');
|
|
147
|
+
const raw = anchored ? filter.slice(1) : filter;
|
|
148
|
+
const parts = raw.split(/\s*[/|]\s*/).map(s => s.trim()).filter(s => s.length > 0);
|
|
149
|
+
if (parts.length > 1) {
|
|
150
|
+
// Multi-segment: use all but last as category, last as test
|
|
151
|
+
args.category = 'Core: ' + parts.slice(0, -1).join(': ');
|
|
152
|
+
args.test = parts[parts.length - 1];
|
|
153
|
+
} else if (parts.length === 1) {
|
|
154
|
+
// Single segment: use as test name filter
|
|
155
|
+
args.test = parts[0];
|
|
156
|
+
}
|
|
157
|
+
color.info(`Filter "${filter}" → category: "${args.category || ''}", test: "${args.test || ''}"`);
|
|
158
|
+
}
|
|
159
|
+
|
|
66
160
|
// If running from a core Dart library directory, delegate to DevTools package
|
|
67
161
|
if (!args.package) {
|
|
68
162
|
const detectedCategory = detectDartLibraryCategory();
|
|
@@ -125,9 +219,7 @@ async function test(args) {
|
|
|
125
219
|
try {
|
|
126
220
|
await testUtils.loadPackages(packagesDir, packageName, args.host, args['skip-publish'], args['skip-build'], args.link);
|
|
127
221
|
} catch (e) {
|
|
128
|
-
console.error('
|
|
129
|
-
// @ts-ignore
|
|
130
|
-
console.error(e.message);
|
|
222
|
+
console.error(e.message || 'Package build/publish failed with no output. Run with --verbose for details.');
|
|
131
223
|
process.exit(1);
|
|
132
224
|
}
|
|
133
225
|
}
|
|
@@ -136,7 +228,7 @@ async function test(args) {
|
|
|
136
228
|
if (args.csv) {
|
|
137
229
|
res.csv = (0, _testUtils.addColumnToCsv)(res.csv, 'stress_test', args['stress-test'] ?? false);
|
|
138
230
|
res.csv = (0, _testUtils.addColumnToCsv)(res.csv, 'benchmark', args.benchmark ?? false);
|
|
139
|
-
(0, _testUtils.saveCsvResults)([res.csv],
|
|
231
|
+
(0, _testUtils.saveCsvResults)([res.csv], resolveCsvPath(args.csv));
|
|
140
232
|
}
|
|
141
233
|
(0, _testUtils.printBrowsersResult)(res, args.verbose);
|
|
142
234
|
if (res.failed) {
|
|
@@ -145,9 +237,12 @@ async function test(args) {
|
|
|
145
237
|
} else testUtils.exitWithCode(0);
|
|
146
238
|
return true;
|
|
147
239
|
}
|
|
240
|
+
function resolveFilter(args) {
|
|
241
|
+
return args.f || (args['_'].length > 1 ? String(args['_'][1]) : undefined);
|
|
242
|
+
}
|
|
148
243
|
function isArgsValid(args) {
|
|
149
244
|
const options = Object.keys(args).slice(1);
|
|
150
|
-
if (args['_'].length >
|
|
245
|
+
if (args['_'].length > 2 || options.length > availableCommandOptions.length || options.length > 0 && !options.every(op => availableCommandOptions.includes(op))) return false;
|
|
151
246
|
return true;
|
|
152
247
|
}
|
|
153
248
|
|
|
@@ -208,13 +303,14 @@ async function runTesting(args) {
|
|
|
208
303
|
debug: args['debug'] ?? false,
|
|
209
304
|
skipToCategory: currentSkipToCategory,
|
|
210
305
|
skipToTest: currentSkipToTest,
|
|
211
|
-
keepBrowserOpen: useRetry
|
|
306
|
+
keepBrowserOpen: useRetry,
|
|
307
|
+
urlParams: args.params
|
|
212
308
|
}, browserId, testInvocationTimeout, browserSession);
|
|
213
309
|
|
|
214
310
|
// Store browser session for potential reuse
|
|
215
311
|
if (r.browserSession) browserSession = r.browserSession;
|
|
216
312
|
if (r.error) {
|
|
217
|
-
|
|
313
|
+
color.error(`\nTest execution failed:`);
|
|
218
314
|
console.log(r.error);
|
|
219
315
|
// Close browser on error
|
|
220
316
|
if (browserSession?.browser) await browserSession.browser.close();
|
|
@@ -326,6 +422,443 @@ function readCSVResultData(data) {
|
|
|
326
422
|
rows: parsed.data
|
|
327
423
|
};
|
|
328
424
|
}
|
|
425
|
+
const DARTIUM_WELL_KNOWN_PATHS = ['C:\\programs\\dartium-win-ia32-stable-1.24.2.0\\chrome.exe', _path.default.join(_os.default.homedir(), 'dartium', 'chrome.exe'), _path.default.join(_os.default.homedir(), 'dartium', 'chrome')];
|
|
426
|
+
const DARTIUM_INACTIVITY_TIMEOUT = 180000; // 3 minutes
|
|
427
|
+
|
|
428
|
+
function resolveDartiumPath(arg) {
|
|
429
|
+
if (typeof arg === 'string' && arg !== 'true') {
|
|
430
|
+
if (!_fs.default.existsSync(arg)) throw new Error(`Dartium not found at: ${arg}`);
|
|
431
|
+
return arg;
|
|
432
|
+
}
|
|
433
|
+
const envPath = process.env.DARTIUM_PATH;
|
|
434
|
+
if (envPath) {
|
|
435
|
+
if (!_fs.default.existsSync(envPath)) throw new Error(`DARTIUM_PATH set but not found: ${envPath}`);
|
|
436
|
+
return envPath;
|
|
437
|
+
}
|
|
438
|
+
for (const p of DARTIUM_WELL_KNOWN_PATHS) {
|
|
439
|
+
if (_fs.default.existsSync(p)) return p;
|
|
440
|
+
}
|
|
441
|
+
throw new Error('Dartium not found. Use --dartium=/path/to/chrome.exe or set DARTIUM_PATH');
|
|
442
|
+
}
|
|
443
|
+
function extractConsoleMessage(line) {
|
|
444
|
+
// Try full format first: "message", source: URL (line)
|
|
445
|
+
const match = line.match(/INFO:CONSOLE\(\d+\)\] "(.*)", source:/);
|
|
446
|
+
if (match) return match[1];
|
|
447
|
+
// Truncated format (long messages): "message" without source suffix
|
|
448
|
+
const truncated = line.match(/INFO:CONSOLE\(\d+\)\] "(.*)"?\s*$/);
|
|
449
|
+
return truncated ? truncated[1] : null;
|
|
450
|
+
}
|
|
451
|
+
function parseAutotestLine(msg) {
|
|
452
|
+
// AUTOTEST_PASS: d4 | Viewers | Inputs (42ms)
|
|
453
|
+
// AUTOTEST_FAIL: d4 | Viewers | Inputs (42ms) - error message
|
|
454
|
+
// AUTOTEST_SKIP: d4 | Viewers | Inputs - reason
|
|
455
|
+
const passMatch = msg.match(/^AUTOTEST_PASS: (.+?) \((\d+)ms\)$/);
|
|
456
|
+
if (passMatch) {
|
|
457
|
+
const parts = passMatch[1].split(' | ');
|
|
458
|
+
return {
|
|
459
|
+
name: passMatch[1],
|
|
460
|
+
category: parts.slice(0, -1).join(' | '),
|
|
461
|
+
testName: parts[parts.length - 1],
|
|
462
|
+
success: true,
|
|
463
|
+
ms: parseInt(passMatch[2]),
|
|
464
|
+
skipped: false,
|
|
465
|
+
error: ''
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const failMatch = msg.match(/^AUTOTEST_FAIL: (.+?) \((\d+)ms\) - (.*)$/);
|
|
469
|
+
if (failMatch) {
|
|
470
|
+
const parts = failMatch[1].split(' | ');
|
|
471
|
+
return {
|
|
472
|
+
name: failMatch[1],
|
|
473
|
+
category: parts.slice(0, -1).join(' | '),
|
|
474
|
+
testName: parts[parts.length - 1],
|
|
475
|
+
success: false,
|
|
476
|
+
ms: parseInt(failMatch[2]),
|
|
477
|
+
skipped: false,
|
|
478
|
+
error: failMatch[3]
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const skipMatch = msg.match(/^AUTOTEST_SKIP: (.+?) - (.*)$/);
|
|
482
|
+
if (skipMatch) {
|
|
483
|
+
const parts = skipMatch[1].split(' | ');
|
|
484
|
+
return {
|
|
485
|
+
name: skipMatch[1],
|
|
486
|
+
category: parts.slice(0, -1).join(' | '),
|
|
487
|
+
testName: parts[parts.length - 1],
|
|
488
|
+
success: true,
|
|
489
|
+
ms: 0,
|
|
490
|
+
skipped: true,
|
|
491
|
+
error: skipMatch[2]
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
function writeFailedTestsLog(results, args) {
|
|
497
|
+
const failures = results.filter(r => !r.success && !r.skipped);
|
|
498
|
+
if (failures.length === 0) return null;
|
|
499
|
+
let logPath;
|
|
500
|
+
if (typeof args.logfailed === 'string' && args.logfailed !== 'true' && args.logfailed.length > 0) logPath = _path.default.resolve(args.logfailed);else logPath = _path.default.join(_os.default.tmpdir(), `grok-test-failures-${Date.now()}.md`);
|
|
501
|
+
const filter = resolveFilter(args) || '';
|
|
502
|
+
const lines = [];
|
|
503
|
+
lines.push(`# Failed Tests Report`);
|
|
504
|
+
lines.push('');
|
|
505
|
+
lines.push(`Date: ${new Date().toISOString()}`);
|
|
506
|
+
if (filter) lines.push(`Filter: ${filter}`);
|
|
507
|
+
lines.push(`Total: ${results.length}, Failed: ${failures.length}`);
|
|
508
|
+
lines.push('');
|
|
509
|
+
for (const f of failures) {
|
|
510
|
+
lines.push(`## ${f.name}`);
|
|
511
|
+
lines.push('');
|
|
512
|
+
lines.push(`**Error:** ${f.error}`);
|
|
513
|
+
lines.push('');
|
|
514
|
+
if (f.stack && f.stack.length > 0) {
|
|
515
|
+
lines.push('**Stack trace:**');
|
|
516
|
+
lines.push('```');
|
|
517
|
+
for (const sl of f.stack) lines.push(sl);
|
|
518
|
+
lines.push('```');
|
|
519
|
+
lines.push('');
|
|
520
|
+
}
|
|
521
|
+
const escapedName = f.name.replace(/\|/g, '/').replace(/\s+/g, ' ').trim();
|
|
522
|
+
lines.push(`**Reproduce:** \`grok test --dartium "${escapedName}"\``);
|
|
523
|
+
lines.push('');
|
|
524
|
+
lines.push('---');
|
|
525
|
+
lines.push('');
|
|
526
|
+
}
|
|
527
|
+
_fs.default.writeFileSync(logPath, lines.join('\n'), 'utf8');
|
|
528
|
+
return logPath;
|
|
529
|
+
}
|
|
530
|
+
async function listTests(args) {
|
|
531
|
+
const config = _jsYaml.default.load(_fs.default.readFileSync(confPath, {
|
|
532
|
+
encoding: 'utf-8'
|
|
533
|
+
}));
|
|
534
|
+
const dartiumPath = resolveDartiumPath(args.dartium || true);
|
|
535
|
+
const filter = args.f || (args['_'].length > 2 ? String(args['_'][2]) : '');
|
|
536
|
+
const {
|
|
537
|
+
url,
|
|
538
|
+
key
|
|
539
|
+
} = testUtils.getDevKey(args.host ?? '');
|
|
540
|
+
const token = await testUtils.getToken(url, key);
|
|
541
|
+
const webUrl = await testUtils.getWebUrl(url, token);
|
|
542
|
+
const urlParams = new URLSearchParams();
|
|
543
|
+
urlParams.set('token', token);
|
|
544
|
+
urlParams.set('listTests', filter);
|
|
545
|
+
urlParams.set('excludePackages', '');
|
|
546
|
+
const testUrl = `${webUrl}/?${urlParams.toString()}`;
|
|
547
|
+
const userDataDir = _path.default.join(_os.default.tmpdir(), 'dartium-grok-test');
|
|
548
|
+
color.info(`Listing tests${filter ? ` matching "${filter}"` : ''}...`);
|
|
549
|
+
const dartium = (0, _child_process.spawn)(dartiumPath, ['--enable-logging=stderr', '--no-first-run', `--user-data-dir=${userDataDir}`, testUrl], {
|
|
550
|
+
stdio: ['ignore', 'ignore', 'pipe']
|
|
551
|
+
});
|
|
552
|
+
return new Promise(resolve => {
|
|
553
|
+
const rl = _readline.default.createInterface({
|
|
554
|
+
input: dartium.stderr
|
|
555
|
+
});
|
|
556
|
+
const tests = [];
|
|
557
|
+
const inactivityCheck = setInterval(() => {
|
|
558
|
+
color.error('Timed out waiting for test list.');
|
|
559
|
+
dartium.kill();
|
|
560
|
+
}, DARTIUM_INACTIVITY_TIMEOUT);
|
|
561
|
+
rl.on('line', line => {
|
|
562
|
+
const msg = extractConsoleMessage(line);
|
|
563
|
+
if (!msg) return;
|
|
564
|
+
if (msg.startsWith('AUTOTEST_LIST_DONE:')) {
|
|
565
|
+
clearInterval(inactivityCheck);
|
|
566
|
+
console.log('');
|
|
567
|
+
for (const t of tests) console.log(t);
|
|
568
|
+
console.log(`\n${tests.length} test(s)`);
|
|
569
|
+
dartium.kill();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (msg.startsWith('AUTOTEST_LIST: ')) tests.push(msg.replace('AUTOTEST_LIST: ', ''));
|
|
573
|
+
});
|
|
574
|
+
dartium.on('close', () => {
|
|
575
|
+
clearInterval(inactivityCheck);
|
|
576
|
+
rl.close();
|
|
577
|
+
resolve(true);
|
|
578
|
+
});
|
|
579
|
+
process.on('SIGINT', () => {
|
|
580
|
+
dartium.kill();
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
/** Runs a single Dartium session with the given URL and collects results. */
|
|
585
|
+
function runDartiumSession(dartiumPath, testUrl, userDataDir, verbose) {
|
|
586
|
+
const dartium = (0, _child_process.spawn)(dartiumPath, ['--enable-logging=stderr', '--no-first-run', `--user-data-dir=${userDataDir}`, testUrl], {
|
|
587
|
+
stdio: ['ignore', 'ignore', 'pipe']
|
|
588
|
+
});
|
|
589
|
+
const results = [];
|
|
590
|
+
let totalExpected = 0;
|
|
591
|
+
let done = false;
|
|
592
|
+
let lastRunningTest = '';
|
|
593
|
+
let lastActivityTime = Date.now();
|
|
594
|
+
return new Promise(resolve => {
|
|
595
|
+
const rl = _readline.default.createInterface({
|
|
596
|
+
input: dartium.stderr
|
|
597
|
+
});
|
|
598
|
+
const inactivityCheck = setInterval(() => {
|
|
599
|
+
if (Date.now() - lastActivityTime > DARTIUM_INACTIVITY_TIMEOUT && !done) {
|
|
600
|
+
process.stdout.write('\r\x1b[K');
|
|
601
|
+
color.error(`\nTest appears stuck (no output for ${DARTIUM_INACTIVITY_TIMEOUT / 1000}s). Killing Dartium.`);
|
|
602
|
+
done = true;
|
|
603
|
+
dartium.kill();
|
|
604
|
+
}
|
|
605
|
+
}, 10000);
|
|
606
|
+
rl.on('line', line => {
|
|
607
|
+
const msg = extractConsoleMessage(line);
|
|
608
|
+
if (!msg || !msg.startsWith('AUTOTEST_')) return;
|
|
609
|
+
lastActivityTime = Date.now();
|
|
610
|
+
if (msg.startsWith('AUTOTEST_START:')) {
|
|
611
|
+
const countMatch = msg.match(/(\d+) test/);
|
|
612
|
+
totalExpected = countMatch ? parseInt(countMatch[1]) : 0;
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (msg.startsWith('AUTOTEST_RUN:')) {
|
|
616
|
+
lastRunningTest = msg.replace('AUTOTEST_RUN: ', '');
|
|
617
|
+
process.stdout.write(`\r\x1b[K \x1b[90m\u25B6 ${lastRunningTest}\x1b[0m`);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (msg.startsWith('AUTOTEST_STACK:')) {
|
|
621
|
+
const stackLine = msg.replace('AUTOTEST_STACK: ', '');
|
|
622
|
+
const lastResult = results.length > 0 ? results[results.length - 1] : null;
|
|
623
|
+
if (lastResult && !lastResult.success && !lastResult.skipped) {
|
|
624
|
+
if (!lastResult.stack) lastResult.stack = [];
|
|
625
|
+
lastResult.stack.push(stackLine);
|
|
626
|
+
}
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (msg.startsWith('AUTOTEST_DONE:')) {
|
|
630
|
+
process.stdout.write('\r\x1b[K');
|
|
631
|
+
done = true;
|
|
632
|
+
dartium.kill();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
const result = parseAutotestLine(msg);
|
|
636
|
+
if (result) {
|
|
637
|
+
process.stdout.write('\r\x1b[K');
|
|
638
|
+
results.push(result);
|
|
639
|
+
lastRunningTest = '';
|
|
640
|
+
if (verbose) {
|
|
641
|
+
if (result.skipped) console.log(` \x1b[33m\u25CB ${result.testName} (skipped: ${result.error})\x1b[0m`);else if (result.success) console.log(` \x1b[32m\u2714 ${result.testName} (${result.ms}ms)\x1b[0m`);else console.log(` \x1b[31m\u274C ${result.testName} (${result.ms}ms) - ${result.error}\x1b[0m`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
dartium.on('close', () => {
|
|
646
|
+
clearInterval(inactivityCheck);
|
|
647
|
+
rl.close();
|
|
648
|
+
resolve({
|
|
649
|
+
results,
|
|
650
|
+
done,
|
|
651
|
+
totalExpected,
|
|
652
|
+
lastRunningTest
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
process.on('SIGINT', () => {
|
|
656
|
+
color.warn('\nInterrupted. Killing Dartium...');
|
|
657
|
+
done = true;
|
|
658
|
+
dartium.kill();
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Gets the full list of test names matching a filter via Dartium. */
|
|
664
|
+
function getDartiumTestList(dartiumPath, webUrl, token, filter, extraParams) {
|
|
665
|
+
const urlParams = new URLSearchParams();
|
|
666
|
+
urlParams.set('token', token);
|
|
667
|
+
urlParams.set('listTests', filter);
|
|
668
|
+
urlParams.set('excludePackages', '');
|
|
669
|
+
if (extraParams) for (const pair of extraParams.split('&')) {
|
|
670
|
+
const [k, ...v] = pair.split('=');
|
|
671
|
+
if (k) urlParams.set(k.trim(), v.join('='));
|
|
672
|
+
}
|
|
673
|
+
const testUrl = `${webUrl}/?${urlParams.toString()}`;
|
|
674
|
+
const userDataDir = _path.default.join(_os.default.tmpdir(), 'dartium-grok-test');
|
|
675
|
+
const dartium = (0, _child_process.spawn)(dartiumPath, ['--enable-logging=stderr', '--no-first-run', `--user-data-dir=${userDataDir}`, testUrl], {
|
|
676
|
+
stdio: ['ignore', 'ignore', 'pipe']
|
|
677
|
+
});
|
|
678
|
+
return new Promise(resolve => {
|
|
679
|
+
const rl = _readline.default.createInterface({
|
|
680
|
+
input: dartium.stderr
|
|
681
|
+
});
|
|
682
|
+
const tests = [];
|
|
683
|
+
const timeout = setTimeout(() => {
|
|
684
|
+
dartium.kill();
|
|
685
|
+
}, DARTIUM_INACTIVITY_TIMEOUT);
|
|
686
|
+
rl.on('line', line => {
|
|
687
|
+
const msg = extractConsoleMessage(line);
|
|
688
|
+
if (!msg) return;
|
|
689
|
+
if (msg.startsWith('AUTOTEST_LIST_DONE:')) {
|
|
690
|
+
clearTimeout(timeout);
|
|
691
|
+
dartium.kill();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (msg.startsWith('AUTOTEST_LIST: ')) tests.push(msg.replace('AUTOTEST_LIST: ', ''));
|
|
695
|
+
});
|
|
696
|
+
dartium.on('close', () => {
|
|
697
|
+
clearTimeout(timeout);
|
|
698
|
+
rl.close();
|
|
699
|
+
resolve(tests);
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function buildDartiumUrl(webUrl, token, filter, args) {
|
|
704
|
+
const urlParams = new URLSearchParams();
|
|
705
|
+
urlParams.set('token', token);
|
|
706
|
+
urlParams.set('tests', filter);
|
|
707
|
+
if (!urlParams.has('excludePackages')) urlParams.set('excludePackages', '');
|
|
708
|
+
if (args.params) for (const pair of args.params.split('&')) {
|
|
709
|
+
const [k, ...v] = pair.split('=');
|
|
710
|
+
if (k) urlParams.set(k.trim(), v.join('='));
|
|
711
|
+
}
|
|
712
|
+
return `${webUrl}/?${urlParams.toString()}`;
|
|
713
|
+
}
|
|
714
|
+
function printDartiumSummary(allResults, args) {
|
|
715
|
+
// Category-level summary
|
|
716
|
+
const categoryResults = new Map();
|
|
717
|
+
const categoryFailures = new Map();
|
|
718
|
+
for (const result of allResults) {
|
|
719
|
+
if (!categoryResults.has(result.category)) categoryResults.set(result.category, {
|
|
720
|
+
passed: 0,
|
|
721
|
+
failed: 0,
|
|
722
|
+
skipped: 0
|
|
723
|
+
});
|
|
724
|
+
const catR = categoryResults.get(result.category);
|
|
725
|
+
if (result.skipped) catR.skipped++;else if (result.success) catR.passed++;else {
|
|
726
|
+
catR.failed++;
|
|
727
|
+
if (!categoryFailures.has(result.category)) categoryFailures.set(result.category, []);
|
|
728
|
+
categoryFailures.get(result.category).push({
|
|
729
|
+
testName: result.testName,
|
|
730
|
+
error: result.error,
|
|
731
|
+
stack: result.stack || []
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
for (const [cat, r] of categoryResults) {
|
|
736
|
+
const skippedSuffix = r.skipped > 0 ? `, \x1b[33m${r.skipped} skipped\x1b[0m` : '';
|
|
737
|
+
if (r.failed > 0) {
|
|
738
|
+
console.log(`\x1b[31m\u274C ${cat}\x1b[31m (\x1b[32m${r.passed} passed${skippedSuffix}\x1b[31m, ${r.failed} failed)\x1b[0m`);
|
|
739
|
+
const failures = categoryFailures.get(cat) || [];
|
|
740
|
+
for (const f of failures) {
|
|
741
|
+
console.log(` \x1b[31m\u274C ${f.testName}\x1b[0m${f.error ? `: ${f.error}` : ''}`);
|
|
742
|
+
for (const sl of f.stack) console.log(` \x1b[90m${sl}\x1b[0m`);
|
|
743
|
+
}
|
|
744
|
+
} else console.log(`\x1b[32m\u2714 ${cat} (${r.passed} passed${skippedSuffix})\x1b[0m`);
|
|
745
|
+
}
|
|
746
|
+
const passed = allResults.filter(r => r.success && !r.skipped).length;
|
|
747
|
+
const failed = allResults.filter(r => !r.success && !r.skipped).length;
|
|
748
|
+
const skipped = allResults.filter(r => r.skipped).length;
|
|
749
|
+
console.log('');
|
|
750
|
+
if (failed > 0) color.error(`Results: ${passed} passed, ${failed} failed${skipped > 0 ? `, ${skipped} skipped` : ''} (${allResults.length} total)`);else color.success(`Results: ${passed} passed${skipped > 0 ? `, ${skipped} skipped` : ''} (${allResults.length} total)`);
|
|
751
|
+
|
|
752
|
+
// CSV
|
|
753
|
+
if (args.csv && allResults.length > 0) {
|
|
754
|
+
const now = new Date().toISOString();
|
|
755
|
+
const csvRows = allResults.map(r => ({
|
|
756
|
+
date: now,
|
|
757
|
+
category: r.category,
|
|
758
|
+
name: r.testName,
|
|
759
|
+
success: r.success,
|
|
760
|
+
result: r.skipped ? r.error : r.success ? 'OK' : r.error,
|
|
761
|
+
ms: r.ms,
|
|
762
|
+
skipped: r.skipped,
|
|
763
|
+
error: r.success ? '' : r.error
|
|
764
|
+
}));
|
|
765
|
+
const csv = Papa.unparse(csvRows);
|
|
766
|
+
const csvPath = resolveCsvPath(args.csv);
|
|
767
|
+
_fs.default.writeFileSync(csvPath, csv, 'utf8');
|
|
768
|
+
color.info(`Saved ${csvPath}`);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Log failed
|
|
772
|
+
if (failed > 0 || args.logfailed) {
|
|
773
|
+
const logPath = writeFailedTestsLog(allResults, args);
|
|
774
|
+
if (logPath) color.warn(`Failed tests log: ${logPath}`);
|
|
775
|
+
}
|
|
776
|
+
const totalTestMs = allResults.reduce((sum, r) => sum + r.ms, 0);
|
|
777
|
+
console.log(`\nPassed tests: ${passed}`);
|
|
778
|
+
console.log(`Failed tests: ${failed}`);
|
|
779
|
+
console.log(`Skipped tests: ${skipped}`);
|
|
780
|
+
console.log(`Total test time: ${(totalTestMs / 1000).toFixed(1)}s`);
|
|
781
|
+
if (failed > 0) testUtils.exitWithCode(1);else testUtils.exitWithCode(0);
|
|
782
|
+
}
|
|
783
|
+
async function testDartium(args) {
|
|
784
|
+
const dartiumPath = resolveDartiumPath(args.dartium);
|
|
785
|
+
color.info(`Using Dartium: ${dartiumPath}`);
|
|
786
|
+
const {
|
|
787
|
+
url,
|
|
788
|
+
key
|
|
789
|
+
} = testUtils.getDevKey(args.host ?? '');
|
|
790
|
+
const token = await testUtils.getToken(url, key);
|
|
791
|
+
const webUrl = await testUtils.getWebUrl(url, token);
|
|
792
|
+
const filter = resolveFilter(args) || '';
|
|
793
|
+
const userDataDir = _path.default.join(_os.default.tmpdir(), 'dartium-grok-test');
|
|
794
|
+
const testUrl = buildDartiumUrl(webUrl, token, filter, args);
|
|
795
|
+
const useRetry = args['no-retry'] === false;
|
|
796
|
+
color.info(`Opening: ${webUrl} (tests: ${filter || 'all'})`);
|
|
797
|
+
|
|
798
|
+
// Main run
|
|
799
|
+
const session = await runDartiumSession(dartiumPath, testUrl, userDataDir, args.verbose ?? false);
|
|
800
|
+
const allResults = [...session.results];
|
|
801
|
+
if (session.done) {
|
|
802
|
+
color.info(`Running ${session.totalExpected} test(s)...\n`);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// If Dartium crashed mid-run and retry is enabled, retry missing tests individually
|
|
806
|
+
if (!session.done && useRetry) {
|
|
807
|
+
const completedNames = new Set(session.results.map(r => r.name));
|
|
808
|
+
|
|
809
|
+
// The test that was running when Dartium crashed
|
|
810
|
+
if (session.lastRunningTest && !completedNames.has(session.lastRunningTest)) {
|
|
811
|
+
allResults.push({
|
|
812
|
+
name: session.lastRunningTest,
|
|
813
|
+
category: session.lastRunningTest.split(/\s*\|\s*/).slice(0, -1).join(' | '),
|
|
814
|
+
testName: session.lastRunningTest.split(/\s*\|\s*/).pop(),
|
|
815
|
+
success: false,
|
|
816
|
+
ms: 0,
|
|
817
|
+
skipped: false,
|
|
818
|
+
error: 'Dartium crashed while running this test',
|
|
819
|
+
stack: []
|
|
820
|
+
});
|
|
821
|
+
completedNames.add(session.lastRunningTest);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Get full test list to find what wasn't run
|
|
825
|
+
color.info('\nDartium crashed. Fetching full test list for retry...');
|
|
826
|
+
const allTests = await getDartiumTestList(dartiumPath, webUrl, token, filter, args.params);
|
|
827
|
+
const missingTests = allTests.filter(t => !completedNames.has(t));
|
|
828
|
+
if (missingTests.length > 0) {
|
|
829
|
+
color.info(`Retrying ${missingTests.length} unexecuted test(s) individually...\n`);
|
|
830
|
+
for (let i = 0; i < missingTests.length; i++) {
|
|
831
|
+
const testName = missingTests[i];
|
|
832
|
+
const shortName = testName.split(' | ').pop();
|
|
833
|
+
process.stdout.write(`\r\x1b[K \x1b[90m[${i + 1}/${missingTests.length}] Retrying: ${shortName}\x1b[0m`);
|
|
834
|
+
|
|
835
|
+
// Build URL for this specific test
|
|
836
|
+
const escapedName = testName.replace(/\|/g, '/').replace(/\s+/g, ' ').trim();
|
|
837
|
+
const retryUrl = buildDartiumUrl(webUrl, token, escapedName, args);
|
|
838
|
+
const retrySession = await runDartiumSession(dartiumPath, retryUrl, userDataDir, args.verbose ?? false);
|
|
839
|
+
if (retrySession.results.length > 0) allResults.push(...retrySession.results);else {
|
|
840
|
+
// Test crashed Dartium again or produced no output
|
|
841
|
+
allResults.push({
|
|
842
|
+
name: testName,
|
|
843
|
+
category: testName.split(/\s*\|\s*/).slice(0, -1).join(' | '),
|
|
844
|
+
testName: testName.split(/\s*\|\s*/).pop(),
|
|
845
|
+
success: false,
|
|
846
|
+
ms: 0,
|
|
847
|
+
skipped: false,
|
|
848
|
+
error: 'Dartium crashed on retry — test consistently crashes the browser',
|
|
849
|
+
stack: []
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
process.stdout.write('\r\x1b[K');
|
|
854
|
+
console.log('');
|
|
855
|
+
}
|
|
856
|
+
} else if (!session.done && !useRetry) {
|
|
857
|
+
color.warn(`Dartium exited before tests completed (${allResults.length}/${session.totalExpected || '?'} tests ran). Use --retry to retry missing tests.`);
|
|
858
|
+
}
|
|
859
|
+
printDartiumSummary(allResults, args);
|
|
860
|
+
return allResults.every(r => r.success || r.skipped);
|
|
861
|
+
}
|
|
329
862
|
async function testRecursive(baseDir, args) {
|
|
330
863
|
const packages = (0, _build.discoverPackages)(baseDir);
|
|
331
864
|
if (packages.length === 0) {
|
|
@@ -365,9 +898,9 @@ function buildTestArgs(args) {
|
|
|
365
898
|
return parts;
|
|
366
899
|
}
|
|
367
900
|
function parseTestOutput(stdout) {
|
|
368
|
-
const passedMatch = stdout.match(/Passed amount:\s*(\d+)/);
|
|
369
|
-
const failedMatch = stdout.match(/Failed amount:\s*(\d+)/);
|
|
370
|
-
const skippedMatch = stdout.match(/Skipped amount:\s*(\d+)/);
|
|
901
|
+
const passedMatch = stdout.match(/Passed (?:amount|tests):\s*(\d+)/);
|
|
902
|
+
const failedMatch = stdout.match(/Failed (?:amount|tests):\s*(\d+)/);
|
|
903
|
+
const skippedMatch = stdout.match(/Skipped (?:amount|tests):\s*(\d+)/);
|
|
371
904
|
if (!passedMatch && !failedMatch) return null;
|
|
372
905
|
return {
|
|
373
906
|
passed: passedMatch ? parseInt(passedMatch[1]) : 0,
|
package/bin/grok.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const argv = require('minimist')(process.argv.slice(2), {
|
|
3
3
|
alias: {k: 'key', h: 'help', r: 'recursive', s: 'silent'},
|
|
4
|
+
boolean: ['dartium'],
|
|
4
5
|
});
|
|
5
6
|
const help = require('./commands/help').help;
|
|
6
7
|
const runAllCommand = require('./utils/utils').runAll;
|
|
@@ -13,6 +14,7 @@ const commands = {
|
|
|
13
14
|
claude: require('./commands/claude').claude,
|
|
14
15
|
config: require('./commands/config').config,
|
|
15
16
|
create: require('./commands/create').create,
|
|
17
|
+
'docker-gen': require('./commands/docker-gen').dockerGen,
|
|
16
18
|
init: require('./commands/init').init,
|
|
17
19
|
link: require('./commands/link').link,
|
|
18
20
|
publish: require('./commands/publish').publish,
|