fix(tstest): preserve streaming console output and correctly buffer incomplete TAP lines

This commit is contained in:
2026-01-19 19:14:05 +00:00
parent ae59b7adf2
commit 46f0a5a8cf
8 changed files with 202 additions and 109 deletions

View File

@@ -18,11 +18,12 @@ export class TapParser {
receivedTests: number = 0;
activeTapTestResult: TapTestResult;
private logger: TsTestLogger;
private protocolParser: ProtocolParser;
private protocolVersion: string | null = null;
private startTime: number;
private lineBuffer: string = '';
/**
* the constructor for TapParser
@@ -71,42 +72,99 @@ export class TapParser {
if (Buffer.isBuffer(logChunk)) {
logChunk = logChunk.toString();
}
const logLineArray = logChunk.split('\n');
if (logLineArray[logLineArray.length - 1] === '') {
logLineArray.pop();
// Prepend any buffered content from previous incomplete line
const fullChunk = this.lineBuffer + logChunk;
this.lineBuffer = '';
// Split into segments by newline
const segments = fullChunk.split('\n');
const lastIndex = segments.length - 1;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const isLastSegment = (i === lastIndex);
const hasNewline = !isLastSegment; // All segments except last had a newline after them
if (hasNewline) {
// Complete line - check if it's a TAP protocol message
const messages = this.protocolParser.parseLine(segment);
if (messages.length > 0) {
// Handle protocol messages
for (const message of messages) {
this._handleProtocolMessage(message, segment);
}
} else {
// Non-protocol complete line - handle as console output
this._handleConsoleOutput(segment, true);
}
} else if (segment) {
// Last segment without newline - could be:
// 1. Partial console output (stream immediately)
// 2. Start of a TAP message (need to buffer for protocol parsing)
// Check if it looks like the start of a TAP protocol message
if (this._looksLikeTapStart(segment)) {
// Buffer it for complete line parsing
this.lineBuffer = segment;
} else {
// Stream immediately as console output (no newline)
this._handleConsoleOutput(segment, false);
}
}
}
}
/**
* Check if text could be the start of a TAP protocol message
*/
private _looksLikeTapStart(text: string): boolean {
return (
text.startsWith('ok ') ||
text.startsWith('not ok ') ||
text.startsWith('1..') ||
text.startsWith('# ') ||
text.startsWith('TAP version ') ||
text.startsWith('⟦TSTEST:') ||
text.startsWith('Bail out!')
);
}
/**
* Handle console output from test, preserving streaming behavior
*/
private _handleConsoleOutput(text: string, hasNewline: boolean) {
// Check for snapshot communication (legacy)
const snapshotMatch = text.match(/###SNAPSHOT###(.+)###SNAPSHOT###/);
if (snapshotMatch) {
const base64Data = snapshotMatch[1];
try {
const snapshotData = JSON.parse(Buffer.from(base64Data, 'base64').toString());
this.handleSnapshot(snapshotData);
} catch (error: any) {
if (this.logger) {
this.logger.testConsoleOutput(`Error parsing snapshot data: ${error.message}`);
}
}
return;
}
// Process each line through the protocol parser
for (const logLine of logLineArray) {
const messages = this.protocolParser.parseLine(logLine);
if (messages.length > 0) {
// Handle protocol messages
for (const message of messages) {
this._handleProtocolMessage(message, logLine);
}
// Add to test result buffer
if (this.activeTapTestResult) {
if (hasNewline) {
this.activeTapTestResult.addLogLine(text);
} else {
// Not a protocol message, handle as console output
if (this.activeTapTestResult) {
this.activeTapTestResult.addLogLine(logLine);
}
// Check for snapshot communication (legacy)
const snapshotMatch = logLine.match(/###SNAPSHOT###(.+)###SNAPSHOT###/);
if (snapshotMatch) {
const base64Data = snapshotMatch[1];
try {
const snapshotData = JSON.parse(Buffer.from(base64Data, 'base64').toString());
this.handleSnapshot(snapshotData);
} catch (error: any) {
if (this.logger) {
this.logger.testConsoleOutput(`Error parsing snapshot data: ${error.message}`);
}
}
} else if (this.logger) {
// This is console output from the test file
this.logger.testConsoleOutput(logLine);
}
this.activeTapTestResult.addLogLineRaw(text);
}
}
// Output to logger with streaming support
if (this.logger) {
if (hasNewline) {
this.logger.testConsoleOutput(text);
} else {
this.logger.testConsoleOutputStreaming(text);
}
}
}
@@ -417,6 +475,11 @@ export class TapParser {
this._processLog(data);
});
childProcessArg.on('exit', async () => {
// Flush any remaining buffered content
if (this.lineBuffer) {
this._handleConsoleOutput(this.lineBuffer, false);
this.lineBuffer = '';
}
await this.evaluateFinalResult();
done.resolve();
});