datagrok-tools 6.1.0 → 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/bin/commands/config.js +42 -6
- package/bin/commands/docker-gen.js +16 -0
- package/bin/commands/help.js +28 -14
- package/bin/commands/publish.js +129 -44
- package/bin/commands/test.js +540 -5
- package/bin/grok.js +2 -0
- package/bin/utils/python-celery-gen.js +247 -0
- package/bin/utils/test-utils.js +8 -4
- 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();
|
|
@@ -134,7 +228,7 @@ async function test(args) {
|
|
|
134
228
|
if (args.csv) {
|
|
135
229
|
res.csv = (0, _testUtils.addColumnToCsv)(res.csv, 'stress_test', args['stress-test'] ?? false);
|
|
136
230
|
res.csv = (0, _testUtils.addColumnToCsv)(res.csv, 'benchmark', args.benchmark ?? false);
|
|
137
|
-
(0, _testUtils.saveCsvResults)([res.csv],
|
|
231
|
+
(0, _testUtils.saveCsvResults)([res.csv], resolveCsvPath(args.csv));
|
|
138
232
|
}
|
|
139
233
|
(0, _testUtils.printBrowsersResult)(res, args.verbose);
|
|
140
234
|
if (res.failed) {
|
|
@@ -143,9 +237,12 @@ async function test(args) {
|
|
|
143
237
|
} else testUtils.exitWithCode(0);
|
|
144
238
|
return true;
|
|
145
239
|
}
|
|
240
|
+
function resolveFilter(args) {
|
|
241
|
+
return args.f || (args['_'].length > 1 ? String(args['_'][1]) : undefined);
|
|
242
|
+
}
|
|
146
243
|
function isArgsValid(args) {
|
|
147
244
|
const options = Object.keys(args).slice(1);
|
|
148
|
-
if (args['_'].length >
|
|
245
|
+
if (args['_'].length > 2 || options.length > availableCommandOptions.length || options.length > 0 && !options.every(op => availableCommandOptions.includes(op))) return false;
|
|
149
246
|
return true;
|
|
150
247
|
}
|
|
151
248
|
|
|
@@ -206,7 +303,8 @@ async function runTesting(args) {
|
|
|
206
303
|
debug: args['debug'] ?? false,
|
|
207
304
|
skipToCategory: currentSkipToCategory,
|
|
208
305
|
skipToTest: currentSkipToTest,
|
|
209
|
-
keepBrowserOpen: useRetry
|
|
306
|
+
keepBrowserOpen: useRetry,
|
|
307
|
+
urlParams: args.params
|
|
210
308
|
}, browserId, testInvocationTimeout, browserSession);
|
|
211
309
|
|
|
212
310
|
// Store browser session for potential reuse
|
|
@@ -324,6 +422,443 @@ function readCSVResultData(data) {
|
|
|
324
422
|
rows: parsed.data
|
|
325
423
|
};
|
|
326
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
|
+
}
|
|
327
862
|
async function testRecursive(baseDir, args) {
|
|
328
863
|
const packages = (0, _build.discoverPackages)(baseDir);
|
|
329
864
|
if (packages.length === 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,
|