tstest/readme.protocol.md

287 lines
6.9 KiB
Markdown

# Improved Internal Protocol Design
## Current Issues with TAP Protocol
1. **Delimiter Conflict**: Using `#` for metadata conflicts with test descriptions containing `#`
2. **Ambiguous Parsing**: No clear boundary between test name and metadata
3. **Limited Extensibility**: Adding new metadata requires regex changes
4. **Mixed Concerns**: Protocol data mixed with human-readable output
## Proposed Internal Protocol v2
### Design Principles
1. **Clear Separation**: Protocol data must be unambiguously separated from user content
2. **Extensibility**: Easy to add new metadata without breaking parsers
3. **Backwards Compatible**: Can coexist with standard TAP for gradual migration
4. **Machine Readable**: Structured format for reliable parsing
5. **Human Friendly**: Still readable in raw form
### Protocol Options
#### Option 1: Special Delimiters
```
ok 1 - test description ::TSTEST:: {"time":123,"retry":0}
not ok 2 - another test ::TSTEST:: {"time":45,"error":"timeout"}
ok 3 - skipped test ::TSTEST:: {"time":0,"skip":"not ready"}
```
**Pros**:
- Simple to implement
- Backwards compatible with TAP parsers (they ignore the suffix)
- Easy to parse with split()
**Cons**:
- Still could conflict if test name contains `::TSTEST::`
- Not standard TAP
#### Option 2: Separate Metadata Lines
```
ok 1 - test description
::METADATA:: {"test":1,"time":123,"retry":0}
not ok 2 - another test
::METADATA:: {"test":2,"time":45,"error":"timeout"}
```
**Pros**:
- Complete separation of concerns
- No chance of conflicts
- Can include arbitrary metadata
**Cons**:
- Requires correlation between lines
- More complex parsing
#### Option 3: YAML Blocks (TAP 13 Compatible)
```
ok 1 - test description
---
time: 123
retry: 0
...
not ok 2 - another test
---
time: 45
error: timeout
stack: |
Error: timeout
at Test.run (test.js:10:5)
...
```
**Pros**:
- Standard TAP 13 feature
- Structured data format
- Human readable
- Extensible
**Cons**:
- More verbose
- YAML parsing overhead
#### Option 4: Binary Protocol Markers (Recommended)
```
ok 1 - test description
␛[TSTEST:eyJ0aW1lIjoxMjMsInJldHJ5IjowfQ==]␛
not ok 2 - another test
␛[TSTEST:eyJ0aW1lIjo0NSwiZXJyb3IiOiJ0aW1lb3V0In0=]␛
```
Using ASCII escape character (␛ = \x1B) with base64 encoded JSON.
**Pros**:
- Zero chance of accidental conflicts
- Compact
- Fast to parse
- Invisible in most terminals
**Cons**:
- Not human readable in raw form
- Requires base64 encoding/decoding
### Recommended Implementation: Hybrid Approach
Use multiple strategies based on context:
1. **For timing and basic metadata**: Use structured delimiters
```
ok 1 - test name ⟦time:123,retry:0⟧
```
2. **For complex data (errors, snapshots)**: Use separate protocol lines
```
ok 1 - test failed
⟦TSTEST:ERROR⟧
{"message":"Assertion failed","stack":"...","diff":"..."}
⟦/TSTEST:ERROR⟧
```
3. **For human-readable output**: Keep standard TAP comments
```
# Test suite: User Authentication
ok 1 - should login
```
### Implementation Plan
#### Phase 1: Parser Enhancement
1. Add new protocol parser alongside existing TAP parser
2. Support both old and new formats during transition
3. Add protocol version negotiation
#### Phase 2: Metadata Structure
```typescript
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>;
}
```
#### Phase 3: Protocol Messages
##### Success Message
```
ok 1 - user authentication works
⟦TSTEST:META:{"time":123,"tags":["auth","unit"]}⟧
```
##### Failure Message
```
not ok 2 - login fails with invalid password
⟦TSTEST:META:{"time":45,"retry":1,"maxRetries":3}⟧
⟦TSTEST:ERROR⟧
{
"message": "Expected 401 but got 500",
"stack": "Error: Expected 401 but got 500\n at Test.run (auth.test.ts:25:10)",
"actual": 500,
"expected": 401
}
⟦/TSTEST:ERROR⟧
```
##### Skip Message
```
ok 3 - database integration test ⟦TSTEST:SKIP:No database connection⟧
```
##### Snapshot Communication
```
⟦TSTEST:SNAPSHOT:user-profile⟧
{
"name": "John Doe",
"email": "john@example.com",
"roles": ["user", "admin"]
}
⟦/TSTEST:SNAPSHOT⟧
```
### Migration Strategy
1. **Version Detection**: First line indicates protocol version
```
⟦TSTEST:PROTOCOL:2.0⟧
TAP version 13
```
2. **Gradual Rollout**:
- v1.10: Add protocol v2 parser, keep v1 generator
- v1.11: Generate v2 by default, v1 with --legacy flag
- v2.0: Remove v1 support
3. **Feature Flags**:
```typescript
tap.settings({
protocol: 'v2', // or 'v1', 'auto'
protocolFeatures: {
structuredErrors: true,
enhancedTiming: true,
binaryMarkers: false
}
});
```
### Benefits of New Protocol
1. **Reliability**: No more regex fragility or description conflicts
2. **Performance**: Faster parsing with clear boundaries
3. **Extensibility**: Easy to add new metadata fields
4. **Debugging**: Rich error information with stack traces and diffs
5. **Integration**: Better IDE and CI/CD tool integration
6. **Forward Compatible**: Room for future enhancements
### Example Parser Implementation
```typescript
class ProtocolV2Parser {
private readonly MARKER_START = '⟦TSTEST:';
private readonly MARKER_END = '⟧';
parseMetadata(line: string): TestMetadata | null {
const start = line.lastIndexOf(this.MARKER_START);
if (start === -1) return null;
const end = line.indexOf(this.MARKER_END, start);
if (end === -1) return null;
const content = line.substring(start + this.MARKER_START.length, end);
const [type, data] = content.split(':', 2);
switch (type) {
case 'META':
return JSON.parse(data);
case 'SKIP':
return { skip: data };
case 'TODO':
return { todo: data };
default:
return null;
}
}
parseTestLine(line: string): ParsedTest {
// First extract any metadata
const metadata = this.parseMetadata(line);
// Then parse the TAP part (without metadata)
const cleanLine = this.removeMetadata(line);
const tapResult = this.parseTAP(cleanLine);
return { ...tapResult, metadata };
}
}
```
### Next Steps
1. Implement proof of concept with basic metadata support
2. Test with real-world test suites for edge cases
3. Benchmark parsing performance
4. Get feedback from users
5. Finalize protocol specification
6. Implement in both tapbundle and tstest