fix(tstest): Fix test timing display issue and update TAP protocol documentation
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
export { tap } from './tapbundle.classes.tap.js';
|
||||
export { TapWrap } from './tapbundle.classes.tapwrap.js';
|
||||
export { webhelpers } from './webhelpers.js';
|
||||
|
||||
// Protocol utilities (for future protocol v2)
|
||||
export * from './tapbundle.protocols.js';
|
||||
export { TapTools } from './tapbundle.classes.taptools.js';
|
||||
|
||||
import { expect } from '@push.rocks/smartexpect';
|
||||
|
226
ts_tapbundle/tapbundle.protocols.ts
Normal file
226
ts_tapbundle/tapbundle.protocols.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* 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}`;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user