196 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			196 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import type { 
 | |
|   ITestResult, 
 | |
|   ITestMetadata, 
 | |
|   IPlanLine, 
 | |
|   ISnapshotData,
 | |
|   IErrorBlock,
 | |
|   ITestEvent
 | |
| } from './protocol.types.js';
 | |
| 
 | |
| import {
 | |
|   PROTOCOL_MARKERS, 
 | |
|   PROTOCOL_VERSION 
 | |
| } from './protocol.types.js';
 | |
| 
 | |
| /**
 | |
|  * ProtocolEmitter generates Protocol V2 messages
 | |
|  * This class is used by tapbundle to emit test results in the new protocol format
 | |
|  */
 | |
| export class ProtocolEmitter {
 | |
|   /**
 | |
|    * Emit protocol version header
 | |
|    */
 | |
|   public emitProtocolHeader(): string {
 | |
|     return `${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.PROTOCOL_PREFIX}${PROTOCOL_VERSION}${PROTOCOL_MARKERS.END}`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Emit TAP version line
 | |
|    */
 | |
|   public emitTapVersion(version: number = 13): string {
 | |
|     return `TAP version ${version}`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Emit test plan
 | |
|    */
 | |
|   public emitPlan(plan: IPlanLine): string {
 | |
|     if (plan.skipAll) {
 | |
|       return `1..0 # Skipped: ${plan.skipAll}`;
 | |
|     }
 | |
|     return `${plan.start}..${plan.end}`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Emit a test result
 | |
|    */
 | |
|   public emitTest(result: ITestResult): string[] {
 | |
|     const lines: string[] = [];
 | |
|     
 | |
|     // Build the basic TAP line
 | |
|     let tapLine = result.ok ? 'ok' : 'not ok';
 | |
|     tapLine += ` ${result.testNumber}`;
 | |
|     tapLine += ` - ${result.description}`;
 | |
|     
 | |
|     // Add directive if present
 | |
|     if (result.directive) {
 | |
|       tapLine += ` # ${result.directive.type.toUpperCase()}`;
 | |
|       if (result.directive.reason) {
 | |
|         tapLine += ` ${result.directive.reason}`;
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     // Add inline metadata for simple cases
 | |
|     if (result.metadata && this.shouldUseInlineMetadata(result.metadata)) {
 | |
|       const metaStr = this.createInlineMetadata(result.metadata);
 | |
|       if (metaStr) {
 | |
|         tapLine += ` ${metaStr}`;
 | |
|       }
 | |
|     }
 | |
|     
 | |
|     lines.push(tapLine);
 | |
|     
 | |
|     // Add block metadata for complex cases
 | |
|     if (result.metadata && !this.shouldUseInlineMetadata(result.metadata)) {
 | |
|       lines.push(...this.createBlockMetadata(result.metadata, result.testNumber));
 | |
|     }
 | |
|     
 | |
|     return lines;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Emit a comment line
 | |
|    */
 | |
|   public emitComment(comment: string): string {
 | |
|     return `# ${comment}`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Emit bailout
 | |
|    */
 | |
|   public emitBailout(reason: string): string {
 | |
|     return `Bail out! ${reason}`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Emit snapshot data
 | |
|    */
 | |
|   public emitSnapshot(snapshot: ISnapshotData): string[] {
 | |
|     const lines: string[] = [];
 | |
|     lines.push(`${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.SNAPSHOT_PREFIX}${snapshot.name}${PROTOCOL_MARKERS.END}`);
 | |
|     
 | |
|     if (snapshot.format === 'json') {
 | |
|       lines.push(JSON.stringify(snapshot.content, null, 2));
 | |
|     } else {
 | |
|       lines.push(String(snapshot.content));
 | |
|     }
 | |
|     
 | |
|     lines.push(`${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.SNAPSHOT_END}${PROTOCOL_MARKERS.END}`);
 | |
|     return lines;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Emit error block
 | |
|    */
 | |
|   public emitError(error: IErrorBlock): string[] {
 | |
|     const lines: string[] = [];
 | |
|     lines.push(`${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.ERROR_PREFIX}${PROTOCOL_MARKERS.END}`);
 | |
|     lines.push(JSON.stringify(error, null, 2));
 | |
|     lines.push(`${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.ERROR_END}${PROTOCOL_MARKERS.END}`);
 | |
|     return lines;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Emit test event
 | |
|    */
 | |
|   public emitEvent(event: ITestEvent): string {
 | |
|     const eventJson = JSON.stringify(event);
 | |
|     return `${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.EVENT_PREFIX}${eventJson}${PROTOCOL_MARKERS.END}`;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Check if metadata should be inline
 | |
|    */
 | |
|   private shouldUseInlineMetadata(metadata: ITestMetadata): boolean {
 | |
|     // Use inline for simple metadata (time, retry, simple skip/todo)
 | |
|     const hasComplexData = metadata.error || 
 | |
|                           metadata.custom || 
 | |
|                           (metadata.tags && metadata.tags.length > 0) ||
 | |
|                           metadata.file ||
 | |
|                           metadata.line;
 | |
|     
 | |
|     return !hasComplexData;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Create inline metadata string
 | |
|    */
 | |
|   private createInlineMetadata(metadata: ITestMetadata): string {
 | |
|     const parts: string[] = [];
 | |
|     
 | |
|     if (metadata.time !== undefined) {
 | |
|       parts.push(`time:${metadata.time}`);
 | |
|     }
 | |
|     
 | |
|     if (metadata.retry !== undefined) {
 | |
|       parts.push(`retry:${metadata.retry}`);
 | |
|     }
 | |
|     
 | |
|     if (metadata.skip) {
 | |
|       return `${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.SKIP_PREFIX}${metadata.skip}${PROTOCOL_MARKERS.END}`;
 | |
|     }
 | |
|     
 | |
|     if (metadata.todo) {
 | |
|       return `${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.TODO_PREFIX}${metadata.todo}${PROTOCOL_MARKERS.END}`;
 | |
|     }
 | |
|     
 | |
|     if (parts.length > 0) {
 | |
|       return `${PROTOCOL_MARKERS.START}${parts.join(',')}${PROTOCOL_MARKERS.END}`;
 | |
|     }
 | |
|     
 | |
|     return '';
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Create block metadata lines
 | |
|    */
 | |
|   private createBlockMetadata(metadata: ITestMetadata, testNumber?: number): string[] {
 | |
|     const lines: string[] = [];
 | |
|     
 | |
|     // Create a clean metadata object without skip/todo (handled inline)
 | |
|     const blockMeta = { ...metadata };
 | |
|     delete blockMeta.skip;
 | |
|     delete blockMeta.todo;
 | |
|     
 | |
|     // Emit metadata block
 | |
|     const metaJson = JSON.stringify(blockMeta);
 | |
|     lines.push(`${PROTOCOL_MARKERS.START}${PROTOCOL_MARKERS.META_PREFIX}${metaJson}${PROTOCOL_MARKERS.END}`);
 | |
|     
 | |
|     // Emit separate error block if present
 | |
|     if (metadata.error) {
 | |
|       lines.push(...this.emitError({ testNumber, error: metadata.error }));
 | |
|     }
 | |
|     
 | |
|     return lines;
 | |
|   }
 | |
| } |