tstest/ts_tapbundle/tapbundle.protocols.ts

226 lines
5.9 KiB
TypeScript

/**
* Internal protocol constants and utilities for improved TAP communication
* between tapbundle and tstest
*/
export const PROTOCOL = {
VERSION: '2.0',
MARKERS: {
START: '⟦TSTEST:',
END: '⟧',
BLOCK_END: '⟦/TSTEST:',
},
TYPES: {
META: 'META',
ERROR: 'ERROR',
SKIP: 'SKIP',
TODO: 'TODO',
SNAPSHOT: 'SNAPSHOT',
PROTOCOL: 'PROTOCOL',
}
} as const;
export interface TestMetadata {
// Timing
time?: number; // milliseconds
startTime?: number; // Unix timestamp
endTime?: number; // Unix timestamp
// Status
skip?: string; // skip reason
todo?: string; // todo reason
retry?: number; // retry attempt
maxRetries?: number; // max retries allowed
// Error details
error?: {
message: string;
stack?: string;
diff?: string;
actual?: any;
expected?: any;
};
// Test context
file?: string; // source file
line?: number; // line number
column?: number; // column number
// Custom data
tags?: string[]; // test tags
custom?: Record<string, any>;
}
export class ProtocolEncoder {
/**
* Encode metadata for inline inclusion
*/
static encodeInline(type: string, data: any): string {
if (typeof data === 'string') {
return `${PROTOCOL.MARKERS.START}${type}:${data}${PROTOCOL.MARKERS.END}`;
}
return `${PROTOCOL.MARKERS.START}${type}:${JSON.stringify(data)}${PROTOCOL.MARKERS.END}`;
}
/**
* Encode block data for multi-line content
*/
static encodeBlock(type: string, data: any): string[] {
const lines: string[] = [];
lines.push(`${PROTOCOL.MARKERS.START}${type}${PROTOCOL.MARKERS.END}`);
if (typeof data === 'string') {
lines.push(data);
} else {
lines.push(JSON.stringify(data, null, 2));
}
lines.push(`${PROTOCOL.MARKERS.BLOCK_END}${type}${PROTOCOL.MARKERS.END}`);
return lines;
}
/**
* Create a TAP line with metadata
*/
static createTestLine(
status: 'ok' | 'not ok',
number: number,
description: string,
metadata?: TestMetadata
): string {
let line = `${status} ${number} - ${description}`;
if (metadata) {
// For skip/todo, use inline format for compatibility
if (metadata.skip) {
line += ` ${this.encodeInline(PROTOCOL.TYPES.SKIP, metadata.skip)}`;
} else if (metadata.todo) {
line += ` ${this.encodeInline(PROTOCOL.TYPES.TODO, metadata.todo)}`;
} else {
// For other metadata, append inline
const metaCopy = { ...metadata };
delete metaCopy.error; // Error details go in separate block
if (Object.keys(metaCopy).length > 0) {
line += ` ${this.encodeInline(PROTOCOL.TYPES.META, metaCopy)}`;
}
}
}
return line;
}
}
export class ProtocolDecoder {
/**
* Extract all protocol markers from a line
*/
static extractMarkers(line: string): Array<{type: string, data: any, start: number, end: number}> {
const markers: Array<{type: string, data: any, start: number, end: number}> = [];
let searchFrom = 0;
while (true) {
const start = line.indexOf(PROTOCOL.MARKERS.START, searchFrom);
if (start === -1) break;
const end = line.indexOf(PROTOCOL.MARKERS.END, start);
if (end === -1) break;
const content = line.substring(start + PROTOCOL.MARKERS.START.length, end);
const colonIndex = content.indexOf(':');
if (colonIndex !== -1) {
const type = content.substring(0, colonIndex);
const dataStr = content.substring(colonIndex + 1);
let data: any;
try {
// Try to parse as JSON first
data = JSON.parse(dataStr);
} catch {
// If not JSON, treat as string
data = dataStr;
}
markers.push({
type,
data,
start,
end: end + PROTOCOL.MARKERS.END.length
});
}
searchFrom = end + 1;
}
return markers;
}
/**
* Remove protocol markers from a line
*/
static cleanLine(line: string): string {
const markers = this.extractMarkers(line);
// Remove markers from end to start to preserve indices
let cleanedLine = line;
for (let i = markers.length - 1; i >= 0; i--) {
const marker = markers[i];
cleanedLine = cleanedLine.substring(0, marker.start) +
cleanedLine.substring(marker.end);
}
return cleanedLine.trim();
}
/**
* Parse a test line and extract metadata
*/
static parseTestLine(line: string): {
cleaned: string;
metadata: TestMetadata;
} {
const markers = this.extractMarkers(line);
const metadata: TestMetadata = {};
for (const marker of markers) {
switch (marker.type) {
case PROTOCOL.TYPES.META:
Object.assign(metadata, marker.data);
break;
case PROTOCOL.TYPES.SKIP:
metadata.skip = marker.data;
break;
case PROTOCOL.TYPES.TODO:
metadata.todo = marker.data;
break;
}
}
return {
cleaned: this.cleanLine(line),
metadata
};
}
/**
* Check if a line starts a protocol block
*/
static isBlockStart(line: string): {isBlock: boolean, type?: string} {
const trimmed = line.trim();
if (trimmed.startsWith(PROTOCOL.MARKERS.START) && trimmed.endsWith(PROTOCOL.MARKERS.END)) {
const content = trimmed.slice(PROTOCOL.MARKERS.START.length, -PROTOCOL.MARKERS.END.length);
if (!content.includes(':')) {
return { isBlock: true, type: content };
}
}
return { isBlock: false };
}
/**
* Check if a line ends a protocol block
*/
static isBlockEnd(line: string, type: string): boolean {
return line.trim() === `${PROTOCOL.MARKERS.BLOCK_END}${type}${PROTOCOL.MARKERS.END}`;
}
}