226 lines
5.9 KiB
TypeScript
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}`;
|
|
}
|
|
} |