@quenty/nevermore-cli 4.43.4 → 4.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/build-scripts/{combine-test-places.luau → combine-test-places.lua} +3 -3
- package/build-scripts/{transform-inject-deploy-metadata.luau → transform-inject-deploy-metadata.lua} +3 -3
- package/dist/commands/batch-command/batch-deploy-command.js +4 -4
- package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
- package/dist/commands/batch-command/batch-test-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-test-command.js +76 -19
- package/dist/commands/batch-command/batch-test-command.js.map +1 -1
- package/dist/commands/deploy-command/deploy-init-utils.d.ts.map +1 -1
- package/dist/commands/deploy-command/deploy-init-utils.js +7 -1
- package/dist/commands/deploy-command/deploy-init-utils.js.map +1 -1
- package/dist/commands/deploy-command/deploy-init.d.ts.map +1 -1
- package/dist/commands/deploy-command/deploy-init.js +24 -7
- package/dist/commands/deploy-command/deploy-init.js.map +1 -1
- package/dist/commands/deploy-command/index.js +1 -1
- package/dist/commands/deploy-command/index.js.map +1 -1
- package/dist/utils/deploy/deploy-github-columns.d.ts +12 -2
- package/dist/utils/deploy/deploy-github-columns.d.ts.map +1 -1
- package/dist/utils/deploy/deploy-github-columns.js +11 -4
- package/dist/utils/deploy/deploy-github-columns.js.map +1 -1
- package/dist/utils/deploy/deploy-metadata.js +1 -1
- package/dist/utils/deploy/deploy-metadata.js.map +1 -1
- package/dist/utils/job-context/base-job-context.d.ts +2 -2
- package/dist/utils/job-context/base-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/base-job-context.js +1 -1
- package/dist/utils/job-context/base-job-context.js.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.d.ts +37 -2
- package/dist/utils/job-context/batch-script-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/batch-script-job-context.js +59 -22
- package/dist/utils/job-context/batch-script-job-context.js.map +1 -1
- package/dist/utils/job-context/cloud-job-context.d.ts +2 -2
- package/dist/utils/job-context/cloud-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/cloud-job-context.js +10 -5
- package/dist/utils/job-context/cloud-job-context.js.map +1 -1
- package/dist/utils/job-context/cloud-job-context.test.d.ts +7 -0
- package/dist/utils/job-context/cloud-job-context.test.d.ts.map +1 -0
- package/dist/utils/job-context/cloud-job-context.test.js +91 -0
- package/dist/utils/job-context/cloud-job-context.test.js.map +1 -0
- package/dist/utils/job-context/job-context.d.ts +70 -2
- package/dist/utils/job-context/job-context.d.ts.map +1 -1
- package/dist/utils/job-context/local-job-context.d.ts +2 -2
- package/dist/utils/job-context/local-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/local-job-context.js +4 -2
- package/dist/utils/job-context/local-job-context.js.map +1 -1
- package/dist/utils/job-context/local-job-context.test.d.ts +7 -0
- package/dist/utils/job-context/local-job-context.test.d.ts.map +1 -0
- package/dist/utils/job-context/local-job-context.test.js +68 -0
- package/dist/utils/job-context/local-job-context.test.js.map +1 -0
- package/dist/utils/open-cloud/open-cloud-client.d.ts +50 -7
- package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.js +49 -13
- package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.test.js +125 -1
- package/dist/utils/open-cloud/open-cloud-client.test.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-logs.characterization.test.d.ts +22 -0
- package/dist/utils/open-cloud/open-cloud-logs.characterization.test.d.ts.map +1 -0
- package/dist/utils/open-cloud/open-cloud-logs.characterization.test.js +202 -0
- package/dist/utils/open-cloud/open-cloud-logs.characterization.test.js.map +1 -0
- package/dist/utils/package-manager-utils.d.ts.map +1 -1
- package/dist/utils/package-manager-utils.js +32 -1
- package/dist/utils/package-manager-utils.js.map +1 -1
- package/dist/utils/package-manager-utils.test.js +25 -0
- package/dist/utils/package-manager-utils.test.js.map +1 -1
- package/dist/utils/testing/batch-run.e2e.test.d.ts +17 -0
- package/dist/utils/testing/batch-run.e2e.test.d.ts.map +1 -0
- package/dist/utils/testing/batch-run.e2e.test.js +343 -0
- package/dist/utils/testing/batch-run.e2e.test.js.map +1 -0
- package/dist/utils/testing/batch-test-runner-template.test.d.ts +11 -0
- package/dist/utils/testing/batch-test-runner-template.test.d.ts.map +1 -0
- package/dist/utils/testing/batch-test-runner-template.test.js +38 -0
- package/dist/utils/testing/batch-test-runner-template.test.js.map +1 -0
- package/dist/utils/testing/log-fetch-stats.d.ts +40 -0
- package/dist/utils/testing/log-fetch-stats.d.ts.map +1 -0
- package/dist/utils/testing/log-fetch-stats.js +18 -0
- package/dist/utils/testing/log-fetch-stats.js.map +1 -0
- package/dist/utils/testing/log-fetch-stats.test.d.ts +2 -0
- package/dist/utils/testing/log-fetch-stats.test.d.ts.map +1 -0
- package/dist/utils/testing/log-fetch-stats.test.js +41 -0
- package/dist/utils/testing/log-fetch-stats.test.js.map +1 -0
- package/dist/utils/testing/parsers/batch-log-parser.d.ts +126 -1
- package/dist/utils/testing/parsers/batch-log-parser.d.ts.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.js +440 -103
- package/dist/utils/testing/parsers/batch-log-parser.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-parser.test.js +494 -1
- package/dist/utils/testing/parsers/batch-log-parser.test.js.map +1 -1
- package/dist/utils/testing/parsers/batch-log-renderer.d.ts +79 -0
- package/dist/utils/testing/parsers/batch-log-renderer.d.ts.map +1 -0
- package/dist/utils/testing/parsers/batch-log-renderer.js +241 -0
- package/dist/utils/testing/parsers/batch-log-renderer.js.map +1 -0
- package/dist/utils/testing/parsers/batch-log-renderer.test.d.ts +2 -0
- package/dist/utils/testing/parsers/batch-log-renderer.test.d.ts.map +1 -0
- package/dist/utils/testing/parsers/batch-log-renderer.test.js +202 -0
- package/dist/utils/testing/parsers/batch-log-renderer.test.js.map +1 -0
- package/dist/utils/testing/runner/combined-project-generator.js +1 -1
- package/dist/utils/testing/runner/combined-project-generator.js.map +1 -1
- package/dist/utils/testing/runner/test-runner.d.ts +24 -0
- package/dist/utils/testing/runner/test-runner.d.ts.map +1 -1
- package/dist/utils/testing/runner/test-runner.js +166 -6
- package/dist/utils/testing/runner/test-runner.js.map +1 -1
- package/dist/utils/testing/runner/test-runner.test.js +228 -2
- package/dist/utils/testing/runner/test-runner.test.js.map +1 -1
- package/dist/utils/testing/structured-test-results.d.ts +77 -0
- package/dist/utils/testing/structured-test-results.d.ts.map +1 -0
- package/dist/utils/testing/structured-test-results.js +140 -0
- package/dist/utils/testing/structured-test-results.js.map +1 -0
- package/dist/utils/testing/structured-test-results.test.d.ts +7 -0
- package/dist/utils/testing/structured-test-results.test.d.ts.map +1 -0
- package/dist/utils/testing/structured-test-results.test.js +179 -0
- package/dist/utils/testing/structured-test-results.test.js.map +1 -0
- package/package.json +6 -6
- package/src/commands/batch-command/batch-deploy-command.ts +4 -4
- package/src/commands/batch-command/batch-test-command.ts +94 -19
- package/src/commands/deploy-command/deploy-init-utils.ts +7 -1
- package/src/commands/deploy-command/deploy-init.ts +41 -11
- package/src/commands/deploy-command/index.ts +1 -1
- package/src/utils/deploy/deploy-github-columns.ts +14 -4
- package/src/utils/deploy/deploy-metadata.ts +1 -1
- package/src/utils/job-context/base-job-context.ts +3 -2
- package/src/utils/job-context/batch-script-job-context.ts +98 -17
- package/src/utils/job-context/cloud-job-context.test.ts +112 -0
- package/src/utils/job-context/cloud-job-context.ts +12 -6
- package/src/utils/job-context/job-context.ts +71 -2
- package/src/utils/job-context/local-job-context.test.ts +89 -0
- package/src/utils/job-context/local-job-context.ts +7 -3
- package/src/utils/open-cloud/open-cloud-client.test.ts +155 -1
- package/src/utils/open-cloud/open-cloud-client.ts +94 -18
- package/src/utils/open-cloud/open-cloud-logs.characterization.test.ts +277 -0
- package/src/utils/package-manager-utils.test.ts +27 -0
- package/src/utils/package-manager-utils.ts +34 -1
- package/src/utils/testing/batch-run.e2e.test.ts +425 -0
- package/src/utils/testing/batch-test-runner-template.test.ts +62 -0
- package/src/utils/testing/log-fetch-stats.test.ts +48 -0
- package/src/utils/testing/log-fetch-stats.ts +55 -0
- package/src/utils/testing/parsers/batch-log-parser.test.ts +660 -1
- package/src/utils/testing/parsers/batch-log-parser.ts +721 -116
- package/src/utils/testing/parsers/batch-log-renderer.test.ts +330 -0
- package/src/utils/testing/parsers/batch-log-renderer.ts +329 -0
- package/src/utils/testing/runner/combined-project-generator.ts +1 -1
- package/src/utils/testing/runner/test-runner.test.ts +332 -2
- package/src/utils/testing/runner/test-runner.ts +231 -9
- package/src/utils/testing/structured-test-results.test.ts +267 -0
- package/src/utils/testing/structured-test-results.ts +199 -0
- package/templates/batch-test-runner.lua +242 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/templates/batch-test-runner.luau +0 -137
- /package/build-scripts/{smoke-test-server.luau → smoke-test-server.lua} +0 -0
- /package/build-scripts/{transform-rojo-merge-place.luau → transform-rojo-merge-place.lua} +0 -0
|
@@ -1,9 +1,89 @@
|
|
|
1
1
|
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
2
|
+
import { isCI } from '@quenty/cli-output-helpers/cli-utils';
|
|
2
3
|
import { evaluateTestOutcome, parseTestCounts, } from '../test-log-parser.js';
|
|
3
4
|
import { formatTracebacks, parseTracebacks } from './traceback-parser.js';
|
|
5
|
+
import { describeLogVolume } from '../log-fetch-stats.js';
|
|
6
|
+
/**
|
|
7
|
+
* How one package in a listing turned out, phrased as its group title is.
|
|
8
|
+
*
|
|
9
|
+
* Kept short: this is a line in a list of dozens, not a report.
|
|
10
|
+
*/
|
|
11
|
+
function describeSlugState(slug, results, slugMap) {
|
|
12
|
+
let packageName;
|
|
13
|
+
for (const [name, candidate] of slugMap) {
|
|
14
|
+
if (candidate === slug) {
|
|
15
|
+
packageName = name;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const result = packageName ? results.get(packageName) : undefined;
|
|
20
|
+
if (!result) {
|
|
21
|
+
return 'no result';
|
|
22
|
+
}
|
|
23
|
+
const counts = result.testCounts
|
|
24
|
+
? ` (${result.testCounts.passed}/${result.testCounts.total})`
|
|
25
|
+
: '';
|
|
26
|
+
return `${result.success ? 'Passed' : 'FAILED'}${counts}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Print one diagnostic, honoring the grouping levels.
|
|
30
|
+
*
|
|
31
|
+
* `useGroups` is passed in rather than looked up, so the whole run agrees about
|
|
32
|
+
* whether it is emitting workflow commands. Asking `isCI()` separately here, in
|
|
33
|
+
* the renderer and in the reporter is three answers to one question.
|
|
34
|
+
*/
|
|
35
|
+
export function emitDiagnostic(level, message, useGroups) {
|
|
36
|
+
if (level === 'group') {
|
|
37
|
+
OutputHelper.startGroup(message, useGroups);
|
|
38
|
+
}
|
|
39
|
+
else if (level === 'endgroup') {
|
|
40
|
+
OutputHelper.endGroup(useGroups);
|
|
41
|
+
}
|
|
42
|
+
else if (level === 'warn') {
|
|
43
|
+
OutputHelper.warn(message);
|
|
44
|
+
}
|
|
45
|
+
else if (level === 'info') {
|
|
46
|
+
OutputHelper.info(message);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
OutputHelper.verbose(message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Report through the caller's sink, or straight out when there is none. */
|
|
53
|
+
function makeReporter(onDiagnostic) {
|
|
54
|
+
if (onDiagnostic) {
|
|
55
|
+
return onDiagnostic;
|
|
56
|
+
}
|
|
57
|
+
return (level, message) => emitDiagnostic(level, message, isCI());
|
|
58
|
+
}
|
|
4
59
|
const BEGIN_MARKER = '===BATCH_TEST_BEGIN ';
|
|
5
60
|
const END_MARKER = '===BATCH_TEST_END ';
|
|
6
61
|
const SUMMARY_MARKER = '===BATCH_TEST_SUMMARY===';
|
|
62
|
+
const MARKER_SUFFIX = '===';
|
|
63
|
+
// Matches "<slug> PASS|FAIL [<durationMs>]" — slugs have no whitespace.
|
|
64
|
+
const END_INNER_PATTERN = /^(\S+)\s+(?:PASS|FAIL)(?:\s+(\d+))?$/;
|
|
65
|
+
/** Read counts off a summary entry, ignoring anything malformed. */
|
|
66
|
+
function readSummaryCounts(entry) {
|
|
67
|
+
const counts = entry.counts;
|
|
68
|
+
if (typeof counts !== 'object' || counts === null) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const fields = [
|
|
72
|
+
'passed',
|
|
73
|
+
'failed',
|
|
74
|
+
'skipped',
|
|
75
|
+
'total',
|
|
76
|
+
'suitesPassed',
|
|
77
|
+
'suitesFailed',
|
|
78
|
+
'suitesTotal',
|
|
79
|
+
];
|
|
80
|
+
for (const field of fields) {
|
|
81
|
+
if (!Number.isFinite(counts[field])) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return counts;
|
|
86
|
+
}
|
|
7
87
|
/**
|
|
8
88
|
* Parse the single batch execution's logs into per-package results.
|
|
9
89
|
*
|
|
@@ -18,100 +98,46 @@ const SUMMARY_MARKER = '===BATCH_TEST_SUMMARY===';
|
|
|
18
98
|
* immune to log reordering. The BEGIN/END markers are used only for splitting logs
|
|
19
99
|
* into per-package sections. Jest failure detection provides a secondary override.
|
|
20
100
|
*/
|
|
21
|
-
export function parseBatchTestLogs(rawLogs, slugMap) {
|
|
101
|
+
export function parseBatchTestLogs(rawLogs, slugMap, options = {}) {
|
|
22
102
|
const results = new Map();
|
|
23
103
|
const lines = rawLogs.split('\n');
|
|
104
|
+
// Reported by every diagnostic below. Each of them is some form of "the
|
|
105
|
+
// output is not what this run should have produced", and none can tell how
|
|
106
|
+
// much of it never arrived without knowing what the fetch collected.
|
|
107
|
+
const logVolume = describeLogVolume(rawLogs, options.logFetchStats);
|
|
108
|
+
const report = makeReporter(options.onDiagnostic);
|
|
24
109
|
// Build reverse map: slug → packageName
|
|
25
110
|
const slugToPackage = new Map();
|
|
26
111
|
for (const [packageName, slug] of slugMap) {
|
|
27
112
|
slugToPackage.set(slug, packageName);
|
|
28
113
|
}
|
|
29
114
|
// ── Pass 1: Extract per-package log sections from markers ──
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
/** Only one section can have lost its head: the one the log was cut inside. */
|
|
43
|
-
let headSectionClaimed = false;
|
|
44
|
-
// Matches "<slug> PASS|FAIL [<durationMs>]" — slugs have no whitespace.
|
|
45
|
-
const endInnerPattern = /^(\S+)\s+(?:PASS|FAIL)(?:\s+(\d+))?$/;
|
|
46
|
-
for (let i = 0; i < lines.length; i++) {
|
|
47
|
-
const trimmed = lines[i].trimEnd();
|
|
48
|
-
if (trimmed.startsWith(BEGIN_MARKER) && trimmed.endsWith('===')) {
|
|
49
|
-
currentSlug = trimmed.slice(BEGIN_MARKER.length, -3);
|
|
50
|
-
currentLines = [];
|
|
51
|
-
beginMarkersSeen++;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (trimmed.startsWith(END_MARKER) && trimmed.endsWith('===')) {
|
|
55
|
-
const inner = trimmed.slice(END_MARKER.length, -3);
|
|
56
|
-
endMarkersSeen++;
|
|
57
|
-
const match = endInnerPattern.exec(inner);
|
|
58
|
-
const endSlug = match ? match[1] : inner;
|
|
59
|
-
const durationStr = match?.[2];
|
|
60
|
-
// A section normally closes on the END matching its own BEGIN.
|
|
61
|
-
const closesOwnSection = endSlug === currentSlug;
|
|
62
|
-
// Open Cloud keeps only the tail of a long run's log, so BEGIN — printed
|
|
63
|
-
// first — can be lost while END and the summary survive. Recovering that
|
|
64
|
-
// means letting an END with no open section claim what precedes it, but
|
|
65
|
-
// only where the head can actually have been dropped: before any BEGIN
|
|
66
|
-
// has survived, and only once. Past that point the log is well-formed,
|
|
67
|
-
// and a BEGIN-less END is a message delivered out of order (the API does
|
|
68
|
-
// not order them), which must not be allowed to claim another package's
|
|
69
|
-
// output.
|
|
70
|
-
const closesDroppedHead = currentSlug === null && beginMarkersSeen === 0 && !headSectionClaimed;
|
|
71
|
-
if (closesOwnSection || closesDroppedHead) {
|
|
72
|
-
logSections.set(endSlug, currentLines.join('\n'));
|
|
73
|
-
if (closesDroppedHead) {
|
|
74
|
-
partialSections.add(endSlug);
|
|
75
|
-
headSectionClaimed = true;
|
|
76
|
-
}
|
|
77
|
-
if (durationStr !== undefined) {
|
|
78
|
-
markerDurations.set(endSlug, parseInt(durationStr, 10));
|
|
79
|
-
}
|
|
80
|
-
currentSlug = null;
|
|
81
|
-
currentLines = [];
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
// Reordered marker from another package. Ignore it without resetting
|
|
85
|
-
// state, so the section it interrupted still closes on its own END.
|
|
86
|
-
strayEndMarkers++;
|
|
87
|
-
}
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (trimmed === SUMMARY_MARKER) {
|
|
91
|
-
// Keep scanning: the summary prints last but is not always delivered last,
|
|
92
|
-
// and a section's END can follow it.
|
|
93
|
-
if (summaryLineIndex < 0) {
|
|
94
|
-
summaryLineIndex = i;
|
|
95
|
-
}
|
|
96
|
-
summaryPayloadPending = true;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
// The summary's JSON payload belongs to no section.
|
|
100
|
-
if (summaryPayloadPending && trimmed.trimStart().startsWith('[')) {
|
|
101
|
-
summaryPayloadPending = false;
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
// Accumulate unconditionally: output that precedes the first surviving
|
|
105
|
-
// BEGIN still belongs to whichever package's END closes it.
|
|
106
|
-
currentLines.push(lines[i]);
|
|
107
|
-
}
|
|
115
|
+
const tokens = tokenizeBatchLog(lines);
|
|
116
|
+
const { sections: logSections, partialSections, markerDurations, strayEndSlugs, orphanedBeginSlugs, } = foldTokensIntoSections(tokens);
|
|
117
|
+
// Diagnostics are read back off the token list rather than counted alongside
|
|
118
|
+
// the fold, so nothing the warnings report can also steer the parse.
|
|
119
|
+
const beginMarkersSeen = tokensOfKind(tokens, 'begin').length;
|
|
120
|
+
const endMarkersSeen = tokensOfKind(tokens, 'end').length;
|
|
121
|
+
const summaryLineIndex = tokensOfKind(tokens, 'summary')[0]?.lineIndex ?? -1;
|
|
122
|
+
const unknownEndSlugs = [
|
|
123
|
+
...new Set(tokensOfKind(tokens, 'end')
|
|
124
|
+
.map((token) => token.slug)
|
|
125
|
+
.filter((slug) => !slugToPackage.has(slug))),
|
|
126
|
+
];
|
|
108
127
|
// ── Pass 2: Parse the JSON summary for authoritative pcall results ──
|
|
109
128
|
const summaryResults = new Map();
|
|
110
129
|
const summaryDurations = new Map();
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
const summaryErrors = new Map();
|
|
131
|
+
const summaryCounts = new Map();
|
|
132
|
+
const summaryRanJest = new Map();
|
|
133
|
+
// The returned copy first: it is the same summary, from the channel that
|
|
134
|
+
// cannot arrive truncated. Only when there is none is the printed one read.
|
|
135
|
+
const returnedEntries = findReturnedSummaryEntries(options.returnValues);
|
|
136
|
+
if (returnedEntries ||
|
|
137
|
+
(summaryLineIndex >= 0 && summaryLineIndex + 1 < lines.length)) {
|
|
138
|
+
const entries = returnedEntries ?? findSummaryEntries(lines, summaryLineIndex + 1);
|
|
113
139
|
if (entries === undefined) {
|
|
114
|
-
|
|
140
|
+
report('verbose', `[batch-log-parser] Failed to parse JSON summary after line ${summaryLineIndex}: ` +
|
|
115
141
|
lines
|
|
116
142
|
.slice(summaryLineIndex + 1, summaryLineIndex + 3)
|
|
117
143
|
.join('\n')
|
|
@@ -123,23 +149,42 @@ export function parseBatchTestLogs(rawLogs, slugMap) {
|
|
|
123
149
|
if (typeof entry.durationMs === 'number') {
|
|
124
150
|
summaryDurations.set(entry.slug, entry.durationMs);
|
|
125
151
|
}
|
|
152
|
+
if (typeof entry.error === 'string' && entry.error.length > 0) {
|
|
153
|
+
summaryErrors.set(entry.slug, entry.error);
|
|
154
|
+
}
|
|
155
|
+
const counts = readSummaryCounts(entry);
|
|
156
|
+
if (counts) {
|
|
157
|
+
summaryCounts.set(entry.slug, counts);
|
|
158
|
+
summaryRanJest.set(entry.slug, entry.ranJest === true);
|
|
159
|
+
}
|
|
126
160
|
}
|
|
127
|
-
// Log any
|
|
161
|
+
// Log any failures the Luau template reported
|
|
128
162
|
const failures = entries.filter((e) => !e.success);
|
|
129
163
|
if (failures.length > 0) {
|
|
130
|
-
console.error(`[batch-log-parser] Luau
|
|
164
|
+
console.error(`[batch-log-parser] Luau runner failures: ${JSON.stringify(failures)}`);
|
|
131
165
|
}
|
|
132
|
-
console.error(`[batch-log-parser] Parsed ${entries.length} summary entries, ${failures.length}
|
|
166
|
+
console.error(`[batch-log-parser] Parsed ${entries.length} summary entries, ${failures.length} reported failures`);
|
|
133
167
|
}
|
|
134
168
|
}
|
|
135
169
|
// ── Warn when the batch produced no recognizable output ──
|
|
136
|
-
if (
|
|
137
|
-
|
|
170
|
+
if (strayEndSlugs.length > 0) {
|
|
171
|
+
report('verbose', `[batch-log-parser] Ignored ${strayEndSlugs.length} out-of-order END marker(s); ` +
|
|
138
172
|
'their sections closed on their own boundaries.');
|
|
139
173
|
}
|
|
174
|
+
// Reported, not acted on: a slug the batch never asked for means the markers
|
|
175
|
+
// and the package list disagree, which no per-package verdict can express.
|
|
176
|
+
if (unknownEndSlugs.length > 0) {
|
|
177
|
+
report('warn', `[batch-log-parser] END marker(s) for slug(s) not in this batch: ` +
|
|
178
|
+
`${unknownEndSlugs.join(', ')}.`);
|
|
179
|
+
}
|
|
180
|
+
if (orphanedBeginSlugs.length > 0) {
|
|
181
|
+
report('warn', `[batch-log-parser] ${orphanedBeginSlugs.length} section(s) reopened by a ` +
|
|
182
|
+
`second BEGIN before their own END arrived (${orphanedBeginSlugs.join(', ')}); ` +
|
|
183
|
+
'the output collected so far was discarded.');
|
|
184
|
+
}
|
|
140
185
|
const noOutputAtAll = logSections.size === 0 && summaryResults.size === 0;
|
|
141
186
|
if (noOutputAtAll) {
|
|
142
|
-
|
|
187
|
+
report('warn', `[batch-log-parser] No batch markers or summary found in logs (${logVolume}). ` +
|
|
143
188
|
'The batch script may not have started or produced output.');
|
|
144
189
|
}
|
|
145
190
|
// The summary is printed last, so it survives a log fetch that dropped the
|
|
@@ -148,15 +193,15 @@ export function parseBatchTestLogs(rawLogs, slugMap) {
|
|
|
148
193
|
// from pcall exit status alone, knowing nothing about what actually ran.
|
|
149
194
|
const lostSectionOutput = logSections.size === 0 && summaryResults.size > 0;
|
|
150
195
|
if (lostSectionOutput) {
|
|
151
|
-
|
|
152
|
-
`
|
|
153
|
-
|
|
154
|
-
`${
|
|
196
|
+
report('warn', `[batch-log-parser] Received output but could not attribute any of it to ` +
|
|
197
|
+
`a package section (${logVolume}; ${beginMarkersSeen} BEGIN, ` +
|
|
198
|
+
`${endMarkersSeen} END markers; summary at line ${summaryLineIndex} of ` +
|
|
199
|
+
`${lines.length}). ` +
|
|
155
200
|
'Test counts are not derived from unattributed output.');
|
|
156
201
|
// BEGIN is printed first and the summary last, so their positions separate
|
|
157
202
|
// the two ways this happens: a log window that dropped the head keeps the
|
|
158
203
|
// summary at the end, while reordered messages put it early.
|
|
159
|
-
|
|
204
|
+
report('warn', summaryLineIndex >= lines.length - 2
|
|
160
205
|
? '[batch-log-parser] Summary sits at the end of the log — consistent with the head being dropped by a log size/retention limit.'
|
|
161
206
|
: '[batch-log-parser] Summary sits early in the log — consistent with messages delivered out of order.');
|
|
162
207
|
}
|
|
@@ -169,6 +214,13 @@ export function parseBatchTestLogs(rawLogs, slugMap) {
|
|
|
169
214
|
// The unattributed stream is the same text for every package, so it is
|
|
170
215
|
// attached once rather than repeated per package across a large log.
|
|
171
216
|
let unattributedClaimed = false;
|
|
217
|
+
/** Packages whose run returned its counts, and those left to log scraping. */
|
|
218
|
+
const structuredSlugs = [];
|
|
219
|
+
const scrapedSlugs = [];
|
|
220
|
+
/** Packages reported as failed by a run whose counts show nothing failed. */
|
|
221
|
+
const unexplainedFailures = [];
|
|
222
|
+
/** Packages judged on returned counts alone, their log section having been lost. */
|
|
223
|
+
const sectionlessButCounted = [];
|
|
172
224
|
for (const [packageName, slug] of slugMap) {
|
|
173
225
|
const attributedLogs = logSections.get(slug);
|
|
174
226
|
let sectionLogs = attributedLogs ?? '';
|
|
@@ -177,36 +229,67 @@ export function parseBatchTestLogs(rawLogs, slugMap) {
|
|
|
177
229
|
unattributedClaimed = true;
|
|
178
230
|
}
|
|
179
231
|
const summarySuccess = summaryResults.get(slug);
|
|
232
|
+
const counts = summaryCounts.get(slug);
|
|
180
233
|
const reasons = [];
|
|
181
|
-
// The
|
|
182
|
-
//
|
|
234
|
+
// The summary verdict is a floor, not the whole verdict — everything below
|
|
235
|
+
// can fail a run it called successful. It covers both a script that threw
|
|
236
|
+
// and one whose returned results said it failed, so the reason comes from
|
|
237
|
+
// the runner rather than being guessed at here.
|
|
183
238
|
let success = summarySuccess ?? false;
|
|
184
239
|
if (summarySuccess === false) {
|
|
185
|
-
reasons.push('the batch runner reported
|
|
240
|
+
reasons.push(summaryErrors.get(slug) ?? 'the batch runner reported this as failed');
|
|
186
241
|
}
|
|
187
242
|
else if (summarySuccess === undefined) {
|
|
188
243
|
// Absent from the summary is a different fault from failing in it, and
|
|
189
244
|
// conflating them sends you hunting for a Luau error that never happened.
|
|
190
245
|
reasons.push('this package is missing from the batch summary');
|
|
191
246
|
}
|
|
247
|
+
// Returned counts are structural, so a summary that called this a pass
|
|
248
|
+
// while reporting failed tests is not believed — no log line involved.
|
|
249
|
+
if (success && counts && (counts.failed > 0 || counts.suitesFailed > 0)) {
|
|
250
|
+
success = false;
|
|
251
|
+
reasons.push(`the batch summary reported a pass alongside ${counts.failed} failed ` +
|
|
252
|
+
`test(s) and ${counts.suitesFailed} failed test suite(s)`);
|
|
253
|
+
}
|
|
254
|
+
// The reverse contradiction: failure reported over counts where nothing
|
|
255
|
+
// failed. Left as a failure but said out loud — a runner reading the wrong
|
|
256
|
+
// field produces exactly this, for every package at once.
|
|
257
|
+
if (!success &&
|
|
258
|
+
counts &&
|
|
259
|
+
counts.failed === 0 &&
|
|
260
|
+
counts.suitesFailed === 0) {
|
|
261
|
+
unexplainedFailures.push(slug);
|
|
262
|
+
}
|
|
192
263
|
if (attributedLogs !== undefined) {
|
|
264
|
+
// Demanding a jest report in the section is a stand-in for proof the
|
|
265
|
+
// runner ran, and it is the first thing a truncated log window costs.
|
|
266
|
+
// Returned counts are that proof directly, so they retire the stand-in —
|
|
267
|
+
// without this the whole structured channel changed nothing in a CI batch,
|
|
268
|
+
// where dozens of packages share one log window and a section keeps its
|
|
269
|
+
// END marker long after its jest summary was dropped.
|
|
193
270
|
const outcome = evaluateTestOutcome(attributedLogs, {
|
|
194
|
-
requireTestReport:
|
|
271
|
+
requireTestReport: counts === undefined,
|
|
195
272
|
});
|
|
196
273
|
if (!outcome.success) {
|
|
197
274
|
success = false;
|
|
198
275
|
reasons.push(...outcome.failureReasons);
|
|
199
276
|
}
|
|
200
277
|
}
|
|
201
|
-
else {
|
|
278
|
+
else if (counts === undefined) {
|
|
202
279
|
// Judged unreadable rather than judged by content: whatever broke
|
|
203
280
|
// attribution is reason enough not to trust which package a line is from.
|
|
204
281
|
success = false;
|
|
205
282
|
reasons.push(`no output could be attributed to this package ` +
|
|
206
|
-
`(${
|
|
283
|
+
`(${logVolume}; ${beginMarkersSeen} BEGIN markers found)`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// The counts say what happened even though the log does not. Tracebacks
|
|
287
|
+
// cannot be checked without the text, though, so this is a narrower
|
|
288
|
+
// verdict than a package with a section gets — hence the warning.
|
|
289
|
+
sectionlessButCounted.push(slug);
|
|
207
290
|
}
|
|
208
291
|
if (partialSections.has(slug)) {
|
|
209
|
-
|
|
292
|
+
report('warn', `[batch-log-parser] ${slug}: section closed by its END marker with no BEGIN — ` +
|
|
210
293
|
'the start of this output was dropped by the log window, so the section is partial.');
|
|
211
294
|
}
|
|
212
295
|
// Attribute by script path, not by log position: a deferred callback can
|
|
@@ -215,16 +298,43 @@ export function parseBatchTestLogs(rawLogs, slugMap) {
|
|
|
215
298
|
const tracebackCount = tracebacks.reduce((sum, t) => sum + t.count, 0);
|
|
216
299
|
const owned = tracebacks.filter((t) => !t.owner || t.owner === slug);
|
|
217
300
|
if (tracebacks.length > owned.length) {
|
|
218
|
-
|
|
301
|
+
report('verbose', `[batch-log-parser] ${slug}: ignored ${tracebacks.length - owned.length} traceback(s) belonging to other packages`);
|
|
219
302
|
}
|
|
220
303
|
if (owned.length > 0) {
|
|
221
|
-
|
|
304
|
+
report('warn', `[batch-log-parser] ${slug}: ${owned.length} distinct traceback(s):\n` +
|
|
222
305
|
formatTracebacks(owned));
|
|
223
306
|
}
|
|
224
307
|
const error = reasons.length > 0 ? reasons.join('; ') : undefined;
|
|
225
|
-
|
|
308
|
+
// Counts the runner returned outrank counts scraped from the section: the
|
|
309
|
+
// same numbers when the log survived, real numbers when it did not.
|
|
310
|
+
const scrapedCounts = attributedLogs
|
|
226
311
|
? parseTestCounts(attributedLogs)
|
|
227
312
|
: undefined;
|
|
313
|
+
const testCounts = counts
|
|
314
|
+
? { passed: counts.passed, failed: counts.failed, total: counts.total }
|
|
315
|
+
: scrapedCounts;
|
|
316
|
+
const countsSource = counts
|
|
317
|
+
? 'returned'
|
|
318
|
+
: scrapedCounts
|
|
319
|
+
? 'scraped'
|
|
320
|
+
: undefined;
|
|
321
|
+
if (counts) {
|
|
322
|
+
structuredSlugs.push(slug);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
scrapedSlugs.push(slug);
|
|
326
|
+
}
|
|
327
|
+
// Two channels reporting the same run must agree. When they do not, one of
|
|
328
|
+
// them is lying about what happened and neither total can be trusted on its
|
|
329
|
+
// own — said out loud because a returned count that is quietly wrong reads
|
|
330
|
+
// like a clean run, which is how a broken structured read stays invisible.
|
|
331
|
+
if (counts && scrapedCounts && counts.total !== scrapedCounts.total) {
|
|
332
|
+
report('warn', `[batch-log-parser] ${slug}: returned counts disagree with the log — ` +
|
|
333
|
+
`the run returned ${counts.passed} passed / ${counts.failed} failed / ` +
|
|
334
|
+
`${counts.total} total, its jest report says ${scrapedCounts.passed} / ` +
|
|
335
|
+
`${scrapedCounts.failed} / ${scrapedCounts.total}. Reporting the returned ` +
|
|
336
|
+
`counts; one of the two channels is wrong.`);
|
|
337
|
+
}
|
|
228
338
|
// Prefer the JSON summary (authoritative, immune to log reordering);
|
|
229
339
|
// fall back to the END-marker value if the summary was truncated.
|
|
230
340
|
const durationMs = summaryDurations.get(slug) ?? markerDurations.get(slug);
|
|
@@ -235,11 +345,238 @@ export function parseBatchTestLogs(rawLogs, slugMap) {
|
|
|
235
345
|
durationMs,
|
|
236
346
|
testCounts,
|
|
237
347
|
tracebackCount,
|
|
348
|
+
countsSource,
|
|
349
|
+
logsLost: attributedLogs === undefined && counts !== undefined,
|
|
350
|
+
testResults: counts
|
|
351
|
+
? {
|
|
352
|
+
success: summarySuccess === true,
|
|
353
|
+
ranJest: summaryRanJest.get(slug) === true,
|
|
354
|
+
...counts,
|
|
355
|
+
failures: [],
|
|
356
|
+
omittedFailures: 0,
|
|
357
|
+
error: summaryErrors.get(slug),
|
|
358
|
+
}
|
|
359
|
+
: undefined,
|
|
238
360
|
error,
|
|
239
361
|
});
|
|
240
362
|
}
|
|
363
|
+
reportCountsProvenance(structuredSlugs, scrapedSlugs, unexplainedFailures, sectionlessButCounted, report, results, slugMap);
|
|
241
364
|
return results;
|
|
242
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Say where this batch's counts came from, every run.
|
|
368
|
+
*
|
|
369
|
+
* The structured channel exists because Open Cloud truncates a long run's logs.
|
|
370
|
+
* A channel that is plumbed but not flowing produces output identical to one
|
|
371
|
+
* that is working, so silence here is not evidence of anything — the count is
|
|
372
|
+
* stated unconditionally and the fallback is a warning, not a debug line.
|
|
373
|
+
*/
|
|
374
|
+
function reportCountsProvenance(structuredSlugs, scrapedSlugs, unexplainedFailures, sectionlessButCounted, report, results, slugMap) {
|
|
375
|
+
const total = structuredSlugs.length + scrapedSlugs.length;
|
|
376
|
+
if (total === 0) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// Not a failure — this is the case the structured channel was built for — but
|
|
380
|
+
// these packages were judged without their log text, so nothing checked them
|
|
381
|
+
// for tracebacks, which jest cannot count and only the log shows.
|
|
382
|
+
if (sectionlessButCounted.length > 0) {
|
|
383
|
+
report('warn', `[batch-log-parser] ${sectionlessButCounted.length} package(s) were judged on ` +
|
|
384
|
+
`their returned counts alone, with no log section to read: ` +
|
|
385
|
+
`${sectionlessButCounted.join(', ')}. Their counts are exact; they were ` +
|
|
386
|
+
`not checked for Luau tracebacks, which only the log shows.`);
|
|
387
|
+
}
|
|
388
|
+
report('info', `[batch-log-parser] Counts returned by the run for ${structuredSlugs.length} ` +
|
|
389
|
+
`of ${total} package(s); ${scrapedSlugs.length} scraped from logs.`);
|
|
390
|
+
// Failing every package while every package's counts are clean is a signature,
|
|
391
|
+
// not a coincidence: it means the runner's verdict, not the tests, is wrong.
|
|
392
|
+
if (unexplainedFailures.length > 0) {
|
|
393
|
+
report('warn', `[batch-log-parser] ${unexplainedFailures.length} of ${total} package(s) were ` +
|
|
394
|
+
`failed by a run whose own counts show nothing failed: ` +
|
|
395
|
+
`${unexplainedFailures.join(', ')}. The failures stand — a runner may know ` +
|
|
396
|
+
`something its counts cannot express — but a verdict no count supports is ` +
|
|
397
|
+
`far more likely to be the runner reading the wrong field.`);
|
|
398
|
+
}
|
|
399
|
+
if (scrapedSlugs.length > 0) {
|
|
400
|
+
// One group rather than a line per package: the packages differ but the
|
|
401
|
+
// finding does not, and 78 restatements of it buried everything else. The
|
|
402
|
+
// state goes in the title so the group can be left collapsed.
|
|
403
|
+
report('group', `${scrapedSlugs.length} of ${total} package(s) fell back to log scraping ` +
|
|
404
|
+
`(no test results returned)`);
|
|
405
|
+
report('warn', `Counts for these came from log text, the channel Open Cloud truncates on ` +
|
|
406
|
+
`long runs. A package that runs its tests through ` +
|
|
407
|
+
`NevermoreTestRunnerUtils reports them as a value without its script ` +
|
|
408
|
+
`changing, so these are packages doing something else (see ` +
|
|
409
|
+
`docs/testing/testing.md).`);
|
|
410
|
+
// Each with its own state: this list is read to find the packages worth
|
|
411
|
+
// looking at, and a column of bare names says nothing about which those are.
|
|
412
|
+
for (const slug of scrapedSlugs) {
|
|
413
|
+
report('info', ` ${slug} — ${describeSlugState(slug, results, slugMap)}`);
|
|
414
|
+
}
|
|
415
|
+
report('endgroup', '');
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Classify every log line, so section splitting reads tokens instead of text.
|
|
420
|
+
*/
|
|
421
|
+
export function tokenizeBatchLog(lines) {
|
|
422
|
+
const tokens = [];
|
|
423
|
+
/** True between the summary marker and its JSON payload. */
|
|
424
|
+
let summaryPayloadPending = false;
|
|
425
|
+
for (let i = 0; i < lines.length; i++) {
|
|
426
|
+
const trimmed = lines[i].trimEnd();
|
|
427
|
+
if (trimmed.startsWith(BEGIN_MARKER) && trimmed.endsWith(MARKER_SUFFIX)) {
|
|
428
|
+
tokens.push({
|
|
429
|
+
kind: 'begin',
|
|
430
|
+
slug: trimmed.slice(BEGIN_MARKER.length, -MARKER_SUFFIX.length),
|
|
431
|
+
});
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (trimmed.startsWith(END_MARKER) && trimmed.endsWith(MARKER_SUFFIX)) {
|
|
435
|
+
tokens.push(parseEndToken(trimmed.slice(END_MARKER.length, -MARKER_SUFFIX.length)));
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (trimmed === SUMMARY_MARKER) {
|
|
439
|
+
// Keep scanning: the summary prints last but is not always delivered last,
|
|
440
|
+
// and a section's END can follow it.
|
|
441
|
+
tokens.push({ kind: 'summary', lineIndex: i });
|
|
442
|
+
summaryPayloadPending = true;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
// The summary's JSON payload belongs to no section.
|
|
446
|
+
if (summaryPayloadPending && trimmed.trimStart().startsWith('[')) {
|
|
447
|
+
summaryPayloadPending = false;
|
|
448
|
+
tokens.push({ kind: 'summaryPayload' });
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
tokens.push({ kind: 'content', line: lines[i] });
|
|
452
|
+
}
|
|
453
|
+
return tokens;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Read an END marker's inner text.
|
|
457
|
+
*
|
|
458
|
+
* The PASS|FAIL verdict and the duration are both optional, so inner text that
|
|
459
|
+
* does not parse is taken whole as the slug rather than dropped.
|
|
460
|
+
*/
|
|
461
|
+
function parseEndToken(inner) {
|
|
462
|
+
const match = END_INNER_PATTERN.exec(inner);
|
|
463
|
+
if (!match) {
|
|
464
|
+
return { kind: 'end', slug: inner };
|
|
465
|
+
}
|
|
466
|
+
const durationStr = match[2];
|
|
467
|
+
return {
|
|
468
|
+
kind: 'end',
|
|
469
|
+
slug: match[1],
|
|
470
|
+
durationMs: durationStr === undefined ? undefined : parseInt(durationStr, 10),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Fold tokens into per-package log sections.
|
|
475
|
+
*
|
|
476
|
+
* Sections close on END, not on the next BEGIN, because END is what survives:
|
|
477
|
+
* see the head-claiming rule below.
|
|
478
|
+
*/
|
|
479
|
+
function foldTokensIntoSections(tokens) {
|
|
480
|
+
const sections = new Map();
|
|
481
|
+
const partialSections = new Set();
|
|
482
|
+
const markerDurations = new Map();
|
|
483
|
+
const strayEndSlugs = [];
|
|
484
|
+
const orphanedBeginSlugs = [];
|
|
485
|
+
let openSlug = null;
|
|
486
|
+
let openLines = [];
|
|
487
|
+
/**
|
|
488
|
+
* Whether the log can still be one whose head was dropped.
|
|
489
|
+
*
|
|
490
|
+
* Open Cloud keeps only the tail of a long run's log, so BEGIN — printed
|
|
491
|
+
* first — can be lost while END and the summary survive. Recovering that
|
|
492
|
+
* means letting an END with no open section claim what precedes it, but
|
|
493
|
+
* only where the head can actually have been dropped: before any BEGIN
|
|
494
|
+
* has survived, and only once. Past that point the log is well-formed,
|
|
495
|
+
* and a BEGIN-less END is a message delivered out of order (the API does
|
|
496
|
+
* not order them), which must not be allowed to claim another package's
|
|
497
|
+
* output.
|
|
498
|
+
*/
|
|
499
|
+
let headClaimable = true;
|
|
500
|
+
for (const token of tokens) {
|
|
501
|
+
switch (token.kind) {
|
|
502
|
+
case 'begin': {
|
|
503
|
+
if (openSlug !== null) {
|
|
504
|
+
orphanedBeginSlugs.push(openSlug);
|
|
505
|
+
}
|
|
506
|
+
openSlug = token.slug;
|
|
507
|
+
openLines = [];
|
|
508
|
+
headClaimable = false;
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
case 'end': {
|
|
512
|
+
// A section normally closes on the END matching its own BEGIN.
|
|
513
|
+
const closesOwnSection = token.slug === openSlug;
|
|
514
|
+
// Mutually exclusive with the above: an unclaimed head means no BEGIN
|
|
515
|
+
// has arrived, so no section is open to match.
|
|
516
|
+
const closesDroppedHead = headClaimable;
|
|
517
|
+
if (!closesOwnSection && !closesDroppedHead) {
|
|
518
|
+
// Reordered marker from another package. Ignore it without resetting
|
|
519
|
+
// state, so the section it interrupted still closes on its own END.
|
|
520
|
+
strayEndSlugs.push(token.slug);
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
sections.set(token.slug, openLines.join('\n'));
|
|
524
|
+
if (closesDroppedHead) {
|
|
525
|
+
partialSections.add(token.slug);
|
|
526
|
+
}
|
|
527
|
+
if (token.durationMs !== undefined) {
|
|
528
|
+
markerDurations.set(token.slug, token.durationMs);
|
|
529
|
+
}
|
|
530
|
+
openSlug = null;
|
|
531
|
+
openLines = [];
|
|
532
|
+
headClaimable = false;
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
case 'content': {
|
|
536
|
+
// Accumulate unconditionally: output that precedes the first surviving
|
|
537
|
+
// BEGIN still belongs to whichever package's END closes it.
|
|
538
|
+
openLines.push(token.line);
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
case 'summary':
|
|
542
|
+
case 'summaryPayload': {
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
sections,
|
|
549
|
+
partialSections,
|
|
550
|
+
markerDurations,
|
|
551
|
+
strayEndSlugs,
|
|
552
|
+
orphanedBeginSlugs,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/** Select the tokens of one kind, for counting and reporting. */
|
|
556
|
+
function tokensOfKind(tokens, kind) {
|
|
557
|
+
return tokens.filter((token) => token.kind === kind);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Find the summary array among a task's return values.
|
|
561
|
+
*
|
|
562
|
+
* The batch script returns the same array it prints. Recognized by shape rather
|
|
563
|
+
* than by position: a run is free to return other things, and an array of
|
|
564
|
+
* objects that all carry a string `slug` is the summary and nothing else.
|
|
565
|
+
*/
|
|
566
|
+
export function findReturnedSummaryEntries(returnValues) {
|
|
567
|
+
for (const value of returnValues ?? []) {
|
|
568
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const looksLikeSummary = value.every((entry) => typeof entry === 'object' &&
|
|
572
|
+
entry !== null &&
|
|
573
|
+
typeof entry.slug === 'string');
|
|
574
|
+
if (looksLikeSummary) {
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
243
580
|
/**
|
|
244
581
|
* Find the summary array in the lines following the summary marker.
|
|
245
582
|
*
|