13 KiB
Test Harness Plan for SDL3 Parser Output
Objective
Create a comprehensive test harness that validates the parser's generated Zig code by:
- Compilation check - Verify the generated code compiles without errors
- Syntax validation - Check that all declarations are syntactically valid
- Type checking - Ensure types are correctly formed
- Completeness - Verify all expected declarations are present
- Regression testing - Detect when parser changes break output
Requirements Analysis
What We're Testing
- Input: SDL3 C header file (SDL_gpu.h)
- Parser: The sdl-parser executable
- Output: Generated Zig code (gpu.zig)
- Dependencies: The output imports "c.zig" which we'll need to mock
Challenges
- Generated code depends on
@import("c.zig")which doesn't exist in test environment - Parser outputs stats to stderr mixed with the actual code
- Need to separate compilation checks from runtime checks
- Should test multiple headers, not just SDL_gpu.h
Test Harness Architecture
Option 1: Stub-Based Testing (RECOMMENDED)
Create a minimal c.zig stub that provides fake SDL C definitions, allowing the generated code to compile in isolation.
Pros:
- Can test compilation without full SDL3 installation
- Fast - no external dependencies
- Can run in CI/CD
- Full control over test environment
Cons:
- Need to maintain c.zig stub
- Won't catch ABI mismatches with real SDL3
Option 2: Integration Testing with Real SDL3
Link against actual SDL3 library and test full compilation chain.
Pros:
- Tests real-world usage
- Catches ABI issues
Cons:
- Requires SDL3 installation
- Slower
- More brittle (breaks when SDL3 updates)
Option 3: Hybrid Approach
Use stub-based testing for CI, integration testing for manual verification.
Recommendation: Start with Option 1 (stub-based), add Option 2 later if needed.
Detailed Plan
Phase 1: Basic Compilation Test
Goal: Verify generated code compiles without syntax errors
Steps:
- Create
test_harness.zig- Main test orchestrator - Create
stubs/c.zig- Minimal SDL C stub - Run parser on SDL_gpu.h
- Strip stats header from output (first 12 lines)
- Attempt to compile with stub c.zig
- Report success/failure
Files to Create:
test_harness/test_harness.zig- Main test runnertest_harness/stubs/c.zig- Minimal C stubstest_harness/build.zig- Build configuration- Update main
build.zigto add test-harness step
Implementation:
// test_harness.zig
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Run parser
const result = try std.ChildProcess.run(.{
.allocator = allocator,
.argv = &[_][]const u8{
"zig-out/bin/sdl-parser",
"../SDL/include/SDL3/SDL_gpu.h",
},
});
defer {
allocator.free(result.stdout);
allocator.free(result.stderr);
}
// Strip stats header (first 12 lines)
const code = try stripHeader(result.stdout, allocator);
defer allocator.free(code);
// Write to test file
try std.fs.cwd().writeFile("test_output/gpu.zig", code);
// Compile test
const compile_result = try std.ChildProcess.run(.{
.allocator = allocator,
.argv = &[_][]const u8{
"zig",
"build-lib",
"test_output/gpu.zig",
"-femit-bin=test_output/gpu.o",
},
});
if (compile_result.term.Exited != 0) {
std.debug.print("Compilation failed:\n{s}\n", .{compile_result.stderr});
return error.CompilationFailed;
}
std.debug.print("✅ Compilation test passed!\n", .{});
}
Phase 2: Declaration Counting Test
Goal: Verify all expected declarations are present
Steps:
- Parse the generated code
- Count opaque types, enums, structs, flags, functions
- Compare against expected counts from parser stats
- Report any mismatches
Implementation:
const DeclarationCounts = struct {
opaque_types: usize,
enums: usize,
structs: usize,
flags: usize,
functions: usize,
};
fn countDeclarations(code: []const u8) DeclarationCounts {
var counts = DeclarationCounts{};
var lines = std.mem.split(u8, code, "\n");
while (lines.next()) |line| {
if (std.mem.indexOf(u8, line, "opaque {}")) |_| {
counts.opaque_types += 1;
} else if (std.mem.indexOf(u8, line, "= enum(c_int)")) |_| {
counts.enums += 1;
} else if (std.mem.indexOf(u8, line, "= extern struct")) |_| {
counts.structs += 1;
} else if (std.mem.indexOf(u8, line, "= packed struct")) |_| {
counts.flags += 1;
} else if (std.mem.indexOf(u8, line, "pub inline fn")) |_| {
counts.functions += 1;
}
}
return counts;
}
Phase 3: Specific Type Tests
Goal: Test specific generated types for correctness
Steps:
- Create test cases for known types
- Import generated code
- Verify type properties (size, alignment, fields)
- Test that enum values are accessible
Implementation:
test "GPUTextureUsageFlags has all fields" {
const gpu = @import("../test_output/gpu.zig");
// These should compile without error
var flags: gpu.GPUTextureUsageFlags = .{};
flags.textureusageSampler = true;
flags.textureusageColorTarget = true;
flags.textureusageDepthStencilTarget = true;
// ... etc
// Check size
try std.testing.expectEqual(@sizeOf(u32), @sizeOf(gpu.GPUTextureUsageFlags));
}
test "GPUPrimitiveType enum values accessible" {
const gpu = @import("../test_output/gpu.zig");
const prim_type = gpu.GPUPrimitiveType.primitivetypeTrianglelist;
try std.testing.expect(prim_type == .primitivetypeTrianglelist);
}
test "No enum values start with numbers" {
const gpu = @import("../test_output/gpu.zig");
// These should compile (would fail if identifiers started with numbers)
_ = gpu.GPUIndexElementSize.indexelementsize16bit;
_ = gpu.GPUSampleCount.samplecount1;
_ = gpu.GPUTextureType.texturetype2d;
}
Phase 4: Golden File Testing
Goal: Detect regressions by comparing against known-good output
Steps:
- Generate "golden" reference file from current working parser
- On subsequent runs, compare output against golden file
- Report differences
- Allow updating golden file when changes are intentional
Implementation:
fn compareWithGolden(generated: []const u8, allocator: Allocator) !void {
const golden = try std.fs.cwd().readFileAlloc(
allocator,
"test_harness/golden/gpu.zig",
10 * 1024 * 1024,
);
defer allocator.free(golden);
if (!std.mem.eql(u8, generated, golden)) {
// Show diff
std.debug.print("Output differs from golden file!\n", .{});
// Option: Use external diff tool
const diff_result = try std.ChildProcess.run(.{
.allocator = allocator,
.argv = &[_][]const u8{
"diff",
"-u",
"test_harness/golden/gpu.zig",
"test_output/gpu.zig",
},
});
std.debug.print("{s}\n", .{diff_result.stdout});
return error.OutputMismatch;
}
}
Phase 5: Multiple Header Testing
Goal: Test parser on multiple SDL3 headers
Headers to Test:
- SDL_gpu.h (primary test case)
- SDL_video.h (different patterns)
- SDL_audio.h (different patterns)
- SDL_events.h (lots of enums)
Implementation:
const TestCase = struct {
header: []const u8,
expected_decls: usize,
expected_opaque: usize,
expected_enums: usize,
};
const test_cases = [_]TestCase{
.{
.header = "../SDL/include/SDL3/SDL_gpu.h",
.expected_decls = 169,
.expected_opaque = 13,
.expected_enums = 24,
},
// Add more headers...
};
pub fn runAllTests() !void {
for (test_cases) |test_case| {
std.debug.print("Testing {s}...\n", .{test_case.header});
try testHeader(test_case);
}
}
Build Integration
Update build.zig
Add test harness steps to main build file:
// In build.zig, add:
// Test harness executable
const test_harness = b.addExecutable(.{
.name = "test-harness",
.root_module = b.createModule(.{
.root_source_file = b.path("test_harness/test_harness.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(test_harness);
// Test harness run step
const run_harness = b.addRunArtifact(test_harness);
run_harness.step.dependOn(b.getInstallStep());
const harness_step = b.step("test-harness", "Run output validation test harness");
harness_step.dependOn(&run_harness.step);
// Create stub c.zig for testing
const create_stub_cmd = b.addSystemCommand(&[_][]const u8{
"mkdir", "-p", "test_output",
});
create_stub_cmd.step.dependOn(b.getInstallStep());
run_harness.step.dependOn(&create_stub_cmd.step);
Directory Structure
lib/sdl3/parser/
├── parser.zig
├── patterns.zig
├── naming.zig
├── codegen.zig
├── types.zig
├── build.zig
├── test_harness/
│ ├── test_harness.zig # Main test orchestrator
│ ├── build.zig # Test harness build config
│ ├── stubs/
│ │ └── c.zig # Minimal SDL C stubs
│ ├── golden/
│ │ └── gpu.zig # Known-good reference output
│ └── tests/
│ ├── compilation_test.zig
│ ├── declaration_test.zig
│ ├── type_test.zig
│ └── regression_test.zig
└── test_output/ # Generated during tests (gitignored)
├── gpu.zig
└── *.o
C Stub Design
Minimal c.zig stub that makes generated code compile:
// test_harness/stubs/c.zig
// Opaque C types (just declarations, no real implementation)
pub const SDL_Window = opaque {};
pub const SDL_GPUDevice = opaque {};
pub const SDL_GPUBuffer = opaque {};
// ... all other SDL_GPU* types
// C functions (empty implementations)
pub fn SDL_CreateGPUDevice(_: bool, _: bool, _: ?*const anyopaque) ?*SDL_GPUDevice {
return null;
}
pub fn SDL_DestroyGPUDevice(_: ?*SDL_GPUDevice) void {}
// ... stub all functions referenced in generated code
Alternative: Use @extern with no linkage for even simpler stubs.
Test Execution Workflow
# 1. Build parser
zig build
# 2. Run test harness
zig build test-harness
# Test harness will:
# - Run parser on SDL_gpu.h
# - Generate test output
# - Compile with stubs
# - Count declarations
# - Compare with golden file
# - Run type tests
# - Report results
Success Criteria
✅ Generated code compiles without errors ✅ All expected declarations present ✅ No invalid identifiers (starting with numbers) ✅ Flag structures have all fields populated ✅ Enum values are accessible ✅ Type sizes match expectations ✅ Output matches golden file (or diff is explained) ✅ Tests run in < 5 seconds ✅ No memory leaks in test harness
Failure Scenarios & Handling
| Scenario | Detection | Recovery |
|---|---|---|
| Parser crashes | Check exit code | Report crash, show stderr |
| Compilation fails | Zig build error | Show compiler errors |
| Missing declarations | Count mismatch | List missing items |
| Invalid identifiers | Compilation error | Parser bug - fix naming.zig |
| Empty flags | Field count check | Parser bug - fix patterns.zig |
| Output regression | Golden file diff | Review changes, update golden if OK |
Future Enhancements
- Performance benchmarking - Track parser speed over time
- Fuzz testing - Generate random C headers
- Integration with SDL3 CI - Auto-test on SDL3 updates
- Coverage reporting - Which C patterns are tested
- Error injection - Test parser error handling
- Multi-platform testing - Test on Windows, macOS, Linux
Implementation Phases
Phase 1: MVP (2 hours)
- Basic compilation test
- C stub creation
- Simple pass/fail reporting
Phase 2: Enhanced (2 hours)
- Declaration counting
- Type-specific tests
- Better error reporting
Phase 3: Regression (1 hour)
- Golden file generation
- Diff reporting
- Update mechanism
Phase 4: Multi-header (1 hour)
- Test multiple SDL3 headers
- Test suite organization
Total Estimated Time: 6 hours
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| C stub maintenance burden | High | Medium | Auto-generate stubs from parser output |
| Golden file becomes stale | Medium | Low | Version control + update script |
| Tests too slow | Low | Medium | Parallel execution, caching |
| False positives | Low | High | Manual review process for failures |
Dependencies
- Zig 0.14+
- SDL3 headers (for source input)
- diff tool (optional, for golden file comparison)
- No runtime dependencies (stubs only)
Deliverables
- ✅ TEST_HARNESS_PLAN.md (this document)
- ⏳ test_harness/test_harness.zig
- ⏳ test_harness/stubs/c.zig
- ⏳ test_harness/build.zig
- ⏳ Updated main build.zig
- ⏳ Golden reference file
- ⏳ README for test harness usage