fix(tstest): preserve streaming console output and correctly buffer incomplete TAP lines
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tstest',
|
||||
version: '3.1.4',
|
||||
version: '3.1.5',
|
||||
description: 'a test utility to run tests that match test/**/*.ts'
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ export class TapTestResult {
|
||||
constructor(public id: number) {}
|
||||
|
||||
/**
|
||||
* adds a logLine to the log buffer of the test
|
||||
* adds a logLine to the log buffer of the test (with newline appended)
|
||||
* @param logLine
|
||||
*/
|
||||
addLogLine(logLine: string) {
|
||||
@@ -19,6 +19,15 @@ export class TapTestResult {
|
||||
this.testLogBuffer = Buffer.concat([this.testLogBuffer, logLineBuffer]);
|
||||
}
|
||||
|
||||
/**
|
||||
* adds raw text to the log buffer without appending newline (for streaming output)
|
||||
* @param text
|
||||
*/
|
||||
addLogLineRaw(text: string) {
|
||||
const logLineBuffer = Buffer.from(text);
|
||||
this.testLogBuffer = Buffer.concat([this.testLogBuffer, logLineBuffer]);
|
||||
}
|
||||
|
||||
setTestResult(testOkArg: boolean) {
|
||||
this.testOk = testOkArg;
|
||||
this.testSettled = true;
|
||||
|
||||
@@ -348,10 +348,10 @@ export class TsTestLogger {
|
||||
}
|
||||
}
|
||||
|
||||
// Console output from test files (non-TAP output)
|
||||
// Console output from test files (non-TAP output) - complete lines
|
||||
testConsoleOutput(message: string) {
|
||||
if (this.options.json) return;
|
||||
|
||||
|
||||
// In verbose mode, show console output immediately
|
||||
if (this.options.verbose) {
|
||||
this.log(this.format(` ${message}`, 'dim'));
|
||||
@@ -359,12 +359,48 @@ export class TsTestLogger {
|
||||
// In non-verbose mode, buffer the logs
|
||||
this.currentTestLogs.push(message);
|
||||
}
|
||||
|
||||
|
||||
// Always log to test file if --logfile is specified
|
||||
if (this.currentTestLogFile) {
|
||||
this.logToTestFile(` ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Streaming console output (preserves original formatting, no newline added)
|
||||
testConsoleOutputStreaming(message: string) {
|
||||
if (this.options.json) return;
|
||||
|
||||
const prefix = ' ';
|
||||
if (this.options.verbose) {
|
||||
// Use process.stdout.write to preserve streaming without adding newlines
|
||||
process.stdout.write(this.format(prefix + message, 'dim'));
|
||||
} else {
|
||||
// Buffer without trailing newline - append to last entry if exists and incomplete
|
||||
if (this.currentTestLogs.length > 0) {
|
||||
// Append to the last buffered entry (for streaming segments)
|
||||
this.currentTestLogs[this.currentTestLogs.length - 1] += message;
|
||||
} else {
|
||||
this.currentTestLogs.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Log to test file without adding newline
|
||||
if (this.currentTestLogFile) {
|
||||
this.logToTestFileRaw(prefix + message);
|
||||
}
|
||||
}
|
||||
|
||||
private logToTestFileRaw(message: string) {
|
||||
try {
|
||||
// Remove ANSI color codes for file logging
|
||||
const cleanMessage = message.replace(/\u001b\[[0-9;]*m/g, '');
|
||||
|
||||
// Append to test log file without adding newline
|
||||
fs.appendFileSync(this.currentTestLogFile, cleanMessage);
|
||||
} catch (error) {
|
||||
// Silently fail to avoid disrupting the test run
|
||||
}
|
||||
}
|
||||
|
||||
// Skipped test file
|
||||
testFileSkipped(filename: string, index: number, total: number, reason: string) {
|
||||
|
||||
Reference in New Issue
Block a user