diff --git a/build.zig.zon b/build.zig.zon
index 4db1042..f7cc801 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -1,6 +1,6 @@
.{
.name = .zig_utils,
- .version = "0.7.1",
+ .version = "0.7.2",
.fingerprint = 0x6dc482caf73c4a75,
.minimum_zig_version = "0.16.0",
diff --git a/src/floyd_warshall_optimized.zig b/src/floyd_warshall_optimized.zig
index 80d8dc9..eb331f5 100644
--- a/src/floyd_warshall_optimized.zig
+++ b/src/floyd_warshall_optimized.zig
@@ -488,12 +488,15 @@ pub fn FloydWarshallOptimized(comptime config: Config) type {
self.processRowSimd(k, i);
}
- // Signal that iteration k is complete for our rows.
- // Only one thread owns each row k (ranges are non-overlapping).
- // That thread signals by adding thread_count to unblock all waiting threads.
- if (k >= start_row and k < end_row) {
- _ = sync_counters[k + 1].fetchAdd(thread_count_u32, .release);
- }
+ // Full barrier: EVERY thread signals completion of iteration k,
+ // and iteration k+1 only starts once all `thread_count` threads
+ // have arrived (counter reaches thread_count). The previous code
+ // had only the owner of row k signal (adding thread_count once),
+ // which is NOT a barrier — a fast thread could enter iteration
+ // k+1 and read the next pivot row (k+1) while ITS owner was still
+ // updating it in iteration k, producing a `dist`/`next` matrix
+ // that's mutually inconsistent (cycles in `next`). See #13.
+ _ = sync_counters[k + 1].fetchAdd(1, .release);
}
}
diff --git a/tests/root.zig b/tests/root.zig
index 08aa72a..01bdf6b 100644
--- a/tests/root.zig
+++ b/tests/root.zig
@@ -1,4 +1,5 @@
const std = @import("std");
+const zig_utils = @import("zig_utils");
pub const vector_test = @import("vector_test.zig");
pub const quad_tree_test = @import("quad_tree_test.zig");
@@ -14,3 +15,47 @@ pub const zon_coercion_test = @import("zon_coercion_test.zig");
test {
std.testing.refAllDecls(@This());
}
+
+// Regression for #13 — kept at the test ROOT (top-level) on purpose: the
+// zspec runner does not currently execute tests nested inside the `*Spec`
+// structs under `zig build test`, so a nested test would compile but never
+// run. The parallel `generate()` barrier was not a full barrier, so threads
+// read a not-yet-finalized pivot row and corrupted `dist`/`next` into cycles
+// (→ infinite path reconstruction, hang + OOM on a 1000-worker colony). This
+// asserts the parallel result is bit-identical to the scalar reference.
+test "parallel Floyd-Warshall matches scalar reference (issue #13)" {
+ const allocator = std.testing.allocator;
+ const FloydWarshallParallel = zig_utils.FloydWarshallParallel;
+ const FloydWarshallScalar = zig_utils.FloydWarshallScalar;
+ const N: u32 = 256; // > 64 → parallel path on multi-core
+ const matrix = @as(usize, N) * @as(usize, N);
+
+ for (0..24) |seed| {
+ var par = FloydWarshallParallel.init(allocator);
+ defer par.deinit();
+ var scal = FloydWarshallScalar.init(allocator);
+ defer scal.deinit();
+ par.resize(N);
+ try par.clean();
+ scal.resize(N);
+ try scal.clean();
+
+ var rng = std.Random.DefaultPrng.init(0xF00D +% seed);
+ const r = rng.random();
+ var e: usize = 0;
+ while (e < N * 4) : (e += 1) {
+ const u = r.intRangeLessThan(u32, 0, N);
+ const v = r.intRangeLessThan(u32, 0, N);
+ if (u == v) continue;
+ const w = r.intRangeLessThan(u32, 1, 20);
+ par.addEdge(u, v, w);
+ scal.addEdge(u, v, w);
+ }
+
+ par.generate();
+ scal.generate();
+
+ try std.testing.expectEqualSlices(u32, scal.dist[0..matrix], par.dist[0..matrix]);
+ try std.testing.expectEqualSlices(u32, scal.next[0..matrix], par.next[0..matrix]);
+ }
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/build.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/build.zig
new file mode 100644
index 0000000..ad338f7
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/build.zig
@@ -0,0 +1,203 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ const target = b.standardTargetOptions(.{});
+ const optimize = b.standardOptimizeOption(.{});
+
+ // Library module — link_libc=true because runner.zig/junit.zig use
+ // std.c.{open,write,close,getenv} on POSIX (and Win32 directly on
+ // Windows). Without this, downstream test binaries that import zspec
+ // get a libc-link error on Linux.
+ const zspec_mod = b.addModule("zspec", .{
+ .root_source_file = b.path("src/zspec.zig"),
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ });
+
+ // Optional ECS integration module
+ const zspec_ecs_mod = b.addModule("zspec-ecs", .{
+ .root_source_file = b.path("src/integrations/ecs.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ // Optional FSM integration module
+ const zspec_fsm_mod = b.addModule("zspec-fsm", .{
+ .root_source_file = b.path("src/integrations/fsm.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ // Unit tests for zspec itself
+ const lib_unit_tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("src/zspec.zig"),
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ }),
+ });
+
+ const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
+
+ // Unit tests for the JUnit XML writer. Lives in its own test exe because
+ // `src/runner.zig` (used as the test_runner for the example/factory test
+ // suites) also imports `junit.zig`; pulling it in via `src/zspec.zig`
+ // would make the same file belong to both the `root` and `zspec` modules.
+ const junit_unit_tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("src/junit.zig"),
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ }),
+ });
+
+ const run_junit_unit_tests = b.addRunArtifact(junit_unit_tests);
+
+ // Example tests using zspec
+ const example_tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("tests/example_test.zig"),
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ .imports = &.{
+ .{ .name = "zspec", .module = zspec_mod },
+ },
+ }),
+ .test_runner = .{ .path = b.path("src/runner.zig"), .mode = .simple },
+ });
+
+ const run_example_tests = b.addRunArtifact(example_tests);
+
+ // Factory union tests (issue #29)
+ const factory_union_tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("tests/factory_union_test.zig"),
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ .imports = &.{
+ .{ .name = "zspec", .module = zspec_mod },
+ },
+ }),
+ .test_runner = .{ .path = b.path("src/runner.zig"), .mode = .simple },
+ });
+
+ const run_factory_union_tests = b.addRunArtifact(factory_union_tests);
+
+ // Factory .zon loading tests (issue #31)
+ const factory_zon_tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("tests/factory_zon_test.zig"),
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ .imports = &.{
+ .{ .name = "zspec", .module = zspec_mod },
+ },
+ }),
+ .test_runner = .{ .path = b.path("src/runner.zig"), .mode = .simple },
+ });
+
+ const run_factory_zon_tests = b.addRunArtifact(factory_zon_tests);
+
+ // Fixture tests (RFC 001 / issue #38)
+ const fixture_tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("tests/fixture_test.zig"),
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ .imports = &.{
+ .{ .name = "zspec", .module = zspec_mod },
+ },
+ }),
+ .test_runner = .{ .path = b.path("src/runner.zig"), .mode = .simple },
+ });
+
+ const run_fixture_tests = b.addRunArtifact(fixture_tests);
+
+ // Regression test for issue #44 — the `.simple` runner must
+ // initialize `std.testing.io_instance` so tests reaching for
+ // `std.testing.io` don't deadlock on linux. Routed through the
+ // `.simple` runner (which contains the fix) on purpose: a
+ // default-runner version of this test would always pass because
+ // the stdlib's runner does the per-test init itself.
+ const testing_io_tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("tests/testing_io_test.zig"),
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ .imports = &.{
+ .{ .name = "zspec", .module = zspec_mod },
+ },
+ }),
+ .test_runner = .{ .path = b.path("src/runner.zig"), .mode = .simple },
+ });
+
+ const run_testing_io_tests = b.addRunArtifact(testing_io_tests);
+
+ const test_step = b.step("test", "Run unit tests");
+ test_step.dependOn(&run_lib_unit_tests.step);
+ test_step.dependOn(&run_junit_unit_tests.step);
+ test_step.dependOn(&run_fixture_tests.step);
+ test_step.dependOn(&run_factory_union_tests.step);
+ test_step.dependOn(&run_factory_zon_tests.step);
+ test_step.dependOn(&run_testing_io_tests.step);
+
+ const example_step = b.step("example", "Run example tests");
+ example_step.dependOn(&run_example_tests.step);
+ example_step.dependOn(&run_factory_union_tests.step);
+ example_step.dependOn(&run_factory_zon_tests.step);
+ example_step.dependOn(&run_fixture_tests.step);
+
+ // Examples - individual example files
+ const example_files = [_]struct { name: []const u8, path: []const u8 }{
+ .{ .name = "examples-basic", .path = "examples/basic_test.zig" },
+ .{ .name = "examples-hooks", .path = "examples/hooks_test.zig" },
+ .{ .name = "examples-let", .path = "examples/let_memoization_test.zig" },
+ .{ .name = "examples-matchers", .path = "examples/matchers_test.zig" },
+ .{ .name = "examples-factory", .path = "examples/factory_test.zig" },
+ .{ .name = "examples-factory-zon", .path = "examples/factory_zon_test.zig" },
+ .{ .name = "examples-fixture", .path = "examples/fixture_test.zig" },
+ .{ .name = "examples-nested", .path = "examples/nested_contexts_test.zig" },
+ .{ .name = "examples-ecs", .path = "examples/ecs_integration_test.zig" },
+ .{ .name = "examples-fsm", .path = "examples/fsm_integration_test.zig" },
+ };
+
+ const examples_all_step = b.step("examples", "Run all examples");
+
+ for (example_files) |ex| {
+ // Integration examples need the optional modules
+ const needs_integrations = std.mem.indexOf(u8, ex.name, "-ecs") != null or
+ std.mem.indexOf(u8, ex.name, "-fsm") != null;
+
+ const imports = if (needs_integrations) &[_]std.Build.Module.Import{
+ .{ .name = "zspec", .module = zspec_mod },
+ .{ .name = "zspec-ecs", .module = zspec_ecs_mod },
+ .{ .name = "zspec-fsm", .module = zspec_fsm_mod },
+ } else &[_]std.Build.Module.Import{
+ .{ .name = "zspec", .module = zspec_mod },
+ };
+
+ const ex_test = b.addTest(.{
+ .root_module = b.createModule(.{
+ .root_source_file = b.path(ex.path),
+ .target = target,
+ .optimize = optimize,
+ .link_libc = true,
+ .imports = imports,
+ }),
+ .test_runner = .{ .path = b.path("src/runner.zig"), .mode = .simple },
+ });
+
+ const run_ex = b.addRunArtifact(ex_test);
+ const ex_step = b.step(ex.name, b.fmt("Run {s}", .{ex.path}));
+ ex_step.dependOn(&run_ex.step);
+ examples_all_step.dependOn(&run_ex.step);
+ }
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/build.zig.zon b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/build.zig.zon
new file mode 100644
index 0000000..8e453f2
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/build.zig.zon
@@ -0,0 +1,13 @@
+.{
+ .name = .zspec,
+ .version = "0.9.1",
+ .fingerprint = 0x940ac7516d8ba28d,
+ .paths = .{
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ "tests",
+ "examples",
+ },
+ .dependencies = .{},
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/basic_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/basic_test.zig
new file mode 100644
index 0000000..73b4faa
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/basic_test.zig
@@ -0,0 +1,27 @@
+//! Basic ZSpec Example
+//!
+//! Demonstrates the simplest way to write tests with ZSpec:
+//! - Importing and using zspec
+//! - Basic test structure
+//! - Simple assertions
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+
+// This test block triggers ZSpec to discover all tests in this file
+test {
+ zspec.runAll(@This());
+}
+
+// Simple standalone tests (not in a context struct)
+test "addition works correctly" {
+ const result = 2 + 2;
+ try expect.equal(result, 4);
+}
+
+test "strings can be compared" {
+ const greeting = "hello";
+ try expect.equal(greeting, "hello");
+}
+
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/ecs_integration_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/ecs_integration_test.zig
new file mode 100644
index 0000000..2dbc624
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/ecs_integration_test.zig
@@ -0,0 +1,516 @@
+//! ECS Integration Example
+//!
+//! Demonstrates how to use ZSpec with zig-ecs (https://github.com/prime31/zig-ecs)
+//! for testing Entity Component Systems.
+//!
+//! Features:
+//! - Factory-based component creation
+//! - Registry setup/teardown in before/after hooks
+//! - Creating entities with multiple components
+//! - Batch entity creation
+//! - Using Let for memoized registry
+//! - ComponentFactory pattern
+//!
+//! NOTE: This example shows the integration pattern but does not actually
+//! import zig-ecs since it's not a dependency. To use this pattern:
+//!
+//! 1. Add zspec and zig-ecs to your build.zig.zon:
+//! .dependencies = .{
+//! .zspec = .{
+//! .url = "https://github.com/apotema/zspec/archive/refs/heads/main.tar.gz",
+//! .hash = "...",
+//! },
+//! .ecs = .{
+//! .url = "https://github.com/prime31/zig-ecs/archive/refs/heads/master.tar.gz",
+//! .hash = "...",
+//! },
+//! },
+//!
+//! 2. In your build.zig, get both modules:
+//! const zspec_dep = b.dependency("zspec", .{ .target = target, .optimize = optimize });
+//! const zspec_mod = zspec_dep.module("zspec");
+//! const zspec_ecs_mod = zspec_dep.module("zspec-ecs"); // Optional ECS integration
+//! const ecs_dep = b.dependency("ecs", .{ .target = target, .optimize = optimize });
+//!
+//! 3. Add to your build.zig test imports:
+//! .imports = &.{
+//! .{ .name = "zspec", .module = zspec_mod },
+//! .{ .name = "zspec-ecs", .module = zspec_ecs_mod },
+//! .{ .name = "zig-ecs", .module = ecs_dep.module("zig-ecs") },
+//! },
+//!
+//! 4. Use the patterns shown below in your tests
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+const Factory = zspec.Factory;
+
+// Import the optional ECS integration module
+const ECS = @import("zspec-ecs");
+
+// Uncomment when you have zig-ecs as a dependency:
+// const ecs = @import("zig-ecs");
+
+test {
+ zspec.runAll(@This());
+}
+
+// =============================================================================
+// Mock ECS Types (for demonstration purposes)
+// In real usage, these would come from @import("zig-ecs")
+// =============================================================================
+
+const MockRegistry = struct {
+ allocator: std.mem.Allocator,
+ next_entity: u32 = 0,
+ positions: std.AutoHashMap(u32, Position) = undefined,
+ velocities: std.AutoHashMap(u32, Velocity) = undefined,
+ healths: std.AutoHashMap(u32, Health) = undefined,
+
+ pub fn init(allocator: std.mem.Allocator) MockRegistry {
+ return .{
+ .allocator = allocator,
+ .positions = std.AutoHashMap(u32, Position).init(allocator),
+ .velocities = std.AutoHashMap(u32, Velocity).init(allocator),
+ .healths = std.AutoHashMap(u32, Health).init(allocator),
+ };
+ }
+
+ pub fn deinit(self: *MockRegistry) void {
+ self.positions.deinit();
+ self.velocities.deinit();
+ self.healths.deinit();
+ }
+
+ pub fn create(self: *MockRegistry) u32 {
+ const entity = self.next_entity;
+ self.next_entity += 1;
+ return entity;
+ }
+
+ pub fn add(self: *MockRegistry, entity: u32, component: anytype) void {
+ const T = @TypeOf(component);
+ if (T == Position) {
+ self.positions.put(entity, component) catch unreachable;
+ } else if (T == Velocity) {
+ self.velocities.put(entity, component) catch unreachable;
+ } else if (T == Health) {
+ self.healths.put(entity, component) catch unreachable;
+ }
+ }
+
+ pub fn get(self: *MockRegistry, comptime T: type, entity: u32) ?T {
+ if (T == Position) {
+ return self.positions.get(entity);
+ } else if (T == Velocity) {
+ return self.velocities.get(entity);
+ } else if (T == Health) {
+ return self.healths.get(entity);
+ }
+ return null;
+ }
+};
+
+// =============================================================================
+// Component Definitions
+// =============================================================================
+
+const Position = struct {
+ x: f32,
+ y: f32,
+};
+
+const Velocity = struct {
+ dx: f32,
+ dy: f32,
+};
+
+const Health = struct {
+ current: i32,
+ max: i32,
+};
+
+const Tag = struct {
+ name: []const u8,
+};
+
+// =============================================================================
+// Factory Definitions
+// =============================================================================
+
+const PositionFactory = Factory.define(Position, .{
+ .x = 0.0,
+ .y = 0.0,
+});
+
+const VelocityFactory = Factory.define(Velocity, .{
+ .dx = 0.0,
+ .dy = 0.0,
+});
+
+const HealthFactory = Factory.define(Health, .{
+ .current = 100,
+ .max = 100,
+});
+
+const TagFactory = Factory.define(Tag, .{
+ .name = Factory.sequenceFmt("Entity-{d}"),
+});
+
+// Factory traits for common entity archetypes
+const MovingPositionFactory = PositionFactory.trait(.{
+ .x = 10.0,
+ .y = 10.0,
+});
+
+const FastVelocityFactory = VelocityFactory.trait(.{
+ .dx = 100.0,
+ .dy = 100.0,
+});
+
+const DamagedHealthFactory = HealthFactory.trait(.{
+ .current = 50,
+});
+
+// =============================================================================
+// Pattern 1: Basic Registry Setup with before/after hooks
+// =============================================================================
+
+pub const BasicRegistrySetup = struct {
+ var registry: *MockRegistry = undefined;
+
+ test "tests:before" {
+ Factory.resetSequences();
+ registry = ECS.createRegistry(MockRegistry);
+ }
+
+ test "tests:after" {
+ ECS.destroyRegistry(registry);
+ }
+
+ test "creates entity with single component" {
+ const entity = ECS.createEntity(registry, .{
+ .position = PositionFactory.build(.{}),
+ });
+
+ const pos = registry.get(Position, entity);
+ try expect.notToBeNull(pos);
+ try expect.equal(pos.?.x, 0.0);
+ try expect.equal(pos.?.y, 0.0);
+ }
+
+ test "creates entity with multiple components" {
+ const entity = ECS.createEntity(registry, .{
+ .position = PositionFactory.build(.{ .x = 5.0, .y = 10.0 }),
+ .velocity = VelocityFactory.build(.{ .dx = 1.0, .dy = 2.0 }),
+ .health = HealthFactory.build(.{}),
+ });
+
+ const pos = registry.get(Position, entity);
+ const vel = registry.get(Velocity, entity);
+ const health = registry.get(Health, entity);
+
+ try expect.notToBeNull(pos);
+ try expect.notToBeNull(vel);
+ try expect.notToBeNull(health);
+ try expect.equal(pos.?.x, 5.0);
+ try expect.equal(vel.?.dx, 1.0);
+ try expect.equal(health.?.current, 100);
+ }
+
+ test "uses factory traits for common archetypes" {
+ const entity = ECS.createEntity(registry, .{
+ .position = MovingPositionFactory.build(.{}),
+ .velocity = FastVelocityFactory.build(.{}),
+ });
+
+ const pos = registry.get(Position, entity);
+ const vel = registry.get(Velocity, entity);
+
+ try expect.equal(pos.?.x, 10.0);
+ try expect.equal(vel.?.dx, 100.0);
+ }
+};
+
+// =============================================================================
+// Pattern 2: Using Let for Memoized Registry
+// =============================================================================
+
+pub const LetBasedRegistry = struct {
+ var arena: std.heap.ArenaAllocator = undefined;
+ var test_alloc: std.mem.Allocator = undefined;
+
+ fn createTestRegistry() *MockRegistry {
+ return ECS.createRegistryWith(MockRegistry, test_alloc);
+ }
+
+ const registry = zspec.Let(*MockRegistry, createTestRegistry);
+
+ test "tests:before" {
+ Factory.resetSequences();
+ arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ test_alloc = arena.allocator();
+ }
+
+ test "tests:after" {
+ ECS.destroyRegistryWith(registry.get(), test_alloc);
+ registry.reset();
+ arena.deinit();
+ }
+
+ test "registry is created lazily" {
+ const reg = registry.get();
+ const entity = ECS.createEntity(reg, .{
+ .position = PositionFactory.build(.{}),
+ });
+
+ try expect.notToBeNull(reg.get(Position, entity));
+ }
+
+ test "registry is shared between operations" {
+ const reg = registry.get();
+ const entity1 = ECS.createEntity(reg, .{
+ .position = PositionFactory.build(.{ .x = 1.0 }),
+ });
+ const entity2 = ECS.createEntity(reg, .{
+ .position = PositionFactory.build(.{ .x = 2.0 }),
+ });
+
+ try expect.equal(registry.get().get(Position, entity1).?.x, 1.0);
+ try expect.equal(registry.get().get(Position, entity2).?.x, 2.0);
+ }
+};
+
+// =============================================================================
+// Pattern 3: Batch Entity Creation
+// =============================================================================
+
+pub const BatchCreation = struct {
+ var registry: *MockRegistry = undefined;
+
+ test "tests:before" {
+ Factory.resetSequences();
+ registry = ECS.createRegistry(MockRegistry);
+ }
+
+ test "tests:after" {
+ ECS.destroyRegistry(registry);
+ }
+
+ test "creates multiple entities with same components" {
+ const entities = ECS.createEntities(registry, 5, .{
+ .position = PositionFactory.build(.{ .x = 10.0 }),
+ });
+ defer std.testing.allocator.free(entities);
+
+ try expect.toHaveLength(entities, 5);
+
+ for (entities) |entity| {
+ const pos = registry.get(Position, entity);
+ try expect.notToBeNull(pos);
+ try expect.equal(pos.?.x, 10.0);
+ }
+ }
+
+ test "creates entities with unique sequential components" {
+ var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ _ = alloc;
+ // Note: createEntitiesUnique demonstrates creating entities with unique
+ // component values using sequences and factory calls
+ }
+};
+
+// =============================================================================
+// Pattern 4: ComponentFactory Pattern
+// =============================================================================
+
+pub const ComponentFactoryPattern = struct {
+ var registry: *MockRegistry = undefined;
+
+ // Define component factories for reusable component builders
+ const PositionComponent = ECS.ComponentFactory(Position, PositionFactory);
+ const VelocityComponent = ECS.ComponentFactory(Velocity, VelocityFactory);
+ const HealthComponent = ECS.ComponentFactory(Health, HealthFactory);
+
+ test "tests:before" {
+ Factory.resetSequences();
+ registry = ECS.createRegistry(MockRegistry);
+ }
+
+ test "tests:after" {
+ ECS.destroyRegistry(registry);
+ }
+
+ test "ComponentFactory builds component data" {
+ const pos = PositionComponent.build(.{ .x = 5.0 });
+ try expect.equal(pos.x, 5.0);
+ }
+
+ test "ComponentFactory attaches to existing entity" {
+ const entity = registry.create();
+
+ PositionComponent.attach(registry, entity, .{ .x = 10.0, .y = 20.0 });
+ VelocityComponent.attach(registry, entity, .{ .dx = 1.0, .dy = 2.0 });
+
+ const pos = registry.get(Position, entity);
+ const vel = registry.get(Velocity, entity);
+
+ try expect.equal(pos.?.x, 10.0);
+ try expect.equal(vel.?.dx, 1.0);
+ }
+
+ test "ComponentFactory creates entity with component" {
+ const entity = PositionComponent.createEntityWith(registry, .{
+ .x = 15.0,
+ .y = 25.0,
+ });
+
+ const pos = registry.get(Position, entity);
+ try expect.equal(pos.?.x, 15.0);
+ try expect.equal(pos.?.y, 25.0);
+ }
+
+ test "ComponentFactory creates multiple entities" {
+ const entities = HealthComponent.createEntitiesWith(registry, 3, .{
+ .current = 75,
+ .max = 100,
+ });
+ defer std.testing.allocator.free(entities);
+
+ try expect.toHaveLength(entities, 3);
+
+ for (entities) |entity| {
+ const health = registry.get(Health, entity);
+ try expect.equal(health.?.current, 75);
+ try expect.equal(health.?.max, 100);
+ }
+ }
+};
+
+// =============================================================================
+// Pattern 5: Practical Game Testing Example
+// =============================================================================
+
+pub const GameScenarios = struct {
+ var registry: *MockRegistry = undefined;
+ var arena: std.heap.ArenaAllocator = undefined;
+ var test_alloc: std.mem.Allocator = undefined;
+
+ test "tests:beforeAll" {
+ Factory.resetSequences();
+ }
+
+ test "tests:before" {
+ registry = ECS.createRegistry(MockRegistry);
+ arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ test_alloc = arena.allocator();
+ }
+
+ test "tests:after" {
+ ECS.destroyRegistry(registry);
+ arena.deinit();
+ }
+
+ test "player entity setup" {
+ const player = ECS.createEntity(registry, .{
+ .position = PositionFactory.build(.{ .x = 0.0, .y = 0.0 }),
+ .velocity = VelocityFactory.build(.{ .dx = 0.0, .dy = 0.0 }),
+ .health = HealthFactory.build(.{ .current = 100, .max = 100 }),
+ });
+
+ // Simulate movement
+ const pos = registry.get(Position, player);
+ try expect.equal(pos.?.x, 0.0);
+
+ // Health check
+ const health = registry.get(Health, player);
+ try expect.equal(health.?.current, 100);
+ }
+
+ test "enemy spawning" {
+ const enemies = ECS.createEntities(registry, 10, .{
+ .position = MovingPositionFactory.build(.{}),
+ .velocity = FastVelocityFactory.build(.{}),
+ .health = HealthFactory.build(.{}),
+ });
+ defer std.testing.allocator.free(enemies);
+
+ try expect.toHaveLength(enemies, 10);
+
+ // All enemies have the expected components
+ for (enemies) |enemy| {
+ try expect.notToBeNull(registry.get(Position, enemy));
+ try expect.notToBeNull(registry.get(Velocity, enemy));
+ try expect.notToBeNull(registry.get(Health, enemy));
+ }
+ }
+
+ test "damaged entity scenario" {
+ const damaged_entity = ECS.createEntity(registry, .{
+ .position = PositionFactory.build(.{}),
+ .health = DamagedHealthFactory.build(.{}),
+ });
+
+ const health = registry.get(Health, damaged_entity);
+ try expect.equal(health.?.current, 50);
+ try expect.equal(health.?.max, 100);
+ }
+
+ test "complex battle scenario" {
+ // Create player
+ const player = ECS.createEntity(registry, .{
+ .position = PositionFactory.build(.{ .x = 0.0, .y = 0.0 }),
+ .health = HealthFactory.build(.{}),
+ });
+
+ // Create enemies at different positions
+ const enemy1 = ECS.createEntity(registry, .{
+ .position = PositionFactory.build(.{ .x = 10.0, .y = 10.0 }),
+ .health = HealthFactory.build(.{}),
+ });
+
+ const enemy2 = ECS.createEntity(registry, .{
+ .position = PositionFactory.build(.{ .x = -10.0, .y = 10.0 }),
+ .health = DamagedHealthFactory.build(.{}),
+ });
+
+ // Verify setup
+ try expect.equal(registry.get(Position, player).?.x, 0.0);
+ try expect.equal(registry.get(Position, enemy1).?.x, 10.0);
+ try expect.equal(registry.get(Position, enemy2).?.x, -10.0);
+ try expect.equal(registry.get(Health, enemy2).?.current, 50);
+
+ // Your game logic tests would go here...
+ }
+};
+
+// =============================================================================
+// Summary
+// =============================================================================
+
+// Key patterns demonstrated:
+//
+// 1. Registry Setup:
+// - Use ECS.createRegistry() in before hooks
+// - Use ECS.destroyRegistry() in after hooks
+// - Or use Let for memoized registry creation
+//
+// 2. Entity Creation:
+// - ECS.createEntity(registry, .{ .comp = Factory.build(.{}) })
+// - ECS.createEntities() for batch creation
+// - ComponentFactory pattern for reusable builders
+//
+// 3. Factory Patterns:
+// - Define component factories with Factory.define()
+// - Use .trait() for common archetypes (player, enemy, etc.)
+// - Use Factory.sequence() for unique IDs
+// - Use Factory.sequenceFmt() for unique names
+//
+// 4. Memory Management:
+// - Use arena allocator in tests to avoid leak reports
+// - Reset sequences in beforeAll or before hooks
+// - Clean up registry in after hooks
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/factory_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/factory_test.zig
new file mode 100644
index 0000000..b719667
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/factory_test.zig
@@ -0,0 +1,353 @@
+//! Factory Example
+//!
+//! Demonstrates ZSpec's Factory module (FactoryBot-like test data generation):
+//! - Factory.define() - Define factories with default values
+//! - Factory.sequence() - Auto-incrementing numeric values
+//! - Factory.sequenceFmt() - Formatted sequence strings
+//! - Factory.lazy() - Computed values
+//! - Factory.assoc() - Nested factory associations
+//! - .trait() - Predefined variants
+//! - .build() / .buildPtr() - Create instances
+//!
+//! NOTE: sequenceFmt allocates strings using the provided allocator.
+//! When using std.testing.allocator, these will be reported as leaks
+//! unless you use an arena allocator for tests that use sequenceFmt.
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+const Factory = zspec.Factory;
+
+test {
+ zspec.runAll(@This());
+}
+
+// Use an arena for tests to avoid memory leak reports from sequenceFmt
+var test_arena: std.heap.ArenaAllocator = undefined;
+var test_alloc: std.mem.Allocator = undefined;
+
+fn setupArena() void {
+ test_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ test_alloc = test_arena.allocator();
+}
+
+fn teardownArena() void {
+ test_arena.deinit();
+}
+
+// =============================================================================
+// Model Definitions
+// =============================================================================
+
+const Address = struct {
+ street: []const u8,
+ city: []const u8,
+ zip: []const u8,
+};
+
+const Company = struct {
+ id: u32,
+ name: []const u8,
+ address: ?*Address,
+};
+
+const User = struct {
+ id: u32,
+ name: []const u8,
+ email: []const u8,
+ age: u8,
+ active: bool,
+ role: []const u8,
+ company: ?*Company,
+};
+
+// =============================================================================
+// Factory Definitions
+// =============================================================================
+
+const AddressFactory = Factory.define(Address, .{
+ .street = "123 Main St",
+ .city = "Springfield",
+ .zip = "12345",
+});
+
+const CompanyFactory = Factory.define(Company, .{
+ .id = Factory.sequence(u32),
+ .name = "Acme Inc",
+ .address = null, // Optional pointer defaults to null
+});
+
+const UserFactory = Factory.define(User, .{
+ .id = Factory.sequence(u32),
+ .name = "John Doe",
+ .email = Factory.sequenceFmt("user{d}@example.com"),
+ .age = 25,
+ .active = true,
+ .role = "user",
+ .company = null,
+});
+
+// Traits - predefined variants
+const AdminFactory = UserFactory.trait(.{
+ .role = "admin",
+});
+
+const InactiveUserFactory = UserFactory.trait(.{
+ .active = false,
+});
+
+const SeniorUserFactory = UserFactory.trait(.{
+ .age = 65,
+ .role = "senior",
+});
+
+// =============================================================================
+// Tests
+// =============================================================================
+
+test "tests:beforeAll" {
+ Factory.resetSequences();
+}
+
+pub const BasicUsage = struct {
+ test "tests:before" {
+ Factory.resetSequences();
+ setupArena();
+ }
+
+ test "tests:after" {
+ teardownArena();
+ }
+
+ test "build creates struct with defaults" {
+ const user = UserFactory.buildWith(test_alloc, .{});
+
+ try expect.equal(user.id, 1);
+ try expect.toBeTrue(std.mem.eql(u8, user.name, "John Doe"));
+ try expect.toBeTrue(std.mem.eql(u8, user.email, "user1@example.com"));
+ try expect.equal(user.age, 25);
+ try expect.toBeTrue(user.active);
+ try expect.toBeTrue(std.mem.eql(u8, user.role, "user"));
+ try expect.toBeNull(user.company);
+ }
+
+ test "build with overrides" {
+ const user = UserFactory.buildWith(test_alloc, .{
+ .name = "Jane Smith",
+ .age = 30,
+ });
+
+ try expect.toBeTrue(std.mem.eql(u8, user.name, "Jane Smith"));
+ try expect.equal(user.age, 30);
+ // Other fields keep defaults
+ try expect.toBeTrue(user.active);
+ }
+
+ test "buildPtr creates heap-allocated pointer" {
+ const user_ptr = UserFactory.buildPtrWith(test_alloc, .{});
+ // No need to free - arena handles it
+
+ try expect.toBeTrue(std.mem.eql(u8, user_ptr.name, "John Doe"));
+ }
+};
+
+pub const Sequences = struct {
+ test "tests:before" {
+ Factory.resetSequences();
+ setupArena();
+ }
+
+ test "tests:after" {
+ teardownArena();
+ }
+
+ test "sequence increments automatically" {
+ const user1 = UserFactory.buildWith(test_alloc, .{});
+ const user2 = UserFactory.buildWith(test_alloc, .{});
+ const user3 = UserFactory.buildWith(test_alloc, .{});
+
+ try expect.equal(user1.id, 1);
+ try expect.equal(user2.id, 2);
+ try expect.equal(user3.id, 3);
+ }
+
+ test "sequenceFmt formats strings" {
+ const user1 = UserFactory.buildWith(test_alloc, .{});
+ const user2 = UserFactory.buildWith(test_alloc, .{});
+
+ try expect.toBeTrue(std.mem.eql(u8, user1.email, "user1@example.com"));
+ try expect.toBeTrue(std.mem.eql(u8, user2.email, "user2@example.com"));
+ }
+
+ test "resetSequences resets all counters" {
+ _ = UserFactory.buildWith(test_alloc, .{});
+ _ = UserFactory.buildWith(test_alloc, .{});
+
+ Factory.resetSequences();
+
+ const user = UserFactory.buildWith(test_alloc, .{});
+ try expect.equal(user.id, 1);
+ try expect.toBeTrue(std.mem.eql(u8, user.email, "user1@example.com"));
+ }
+};
+
+pub const Traits = struct {
+ test "tests:before" {
+ Factory.resetSequences();
+ setupArena();
+ }
+
+ test "tests:after" {
+ teardownArena();
+ }
+
+ test "admin trait sets role" {
+ const admin = AdminFactory.buildWith(test_alloc, .{});
+
+ try expect.toBeTrue(std.mem.eql(u8, admin.role, "admin"));
+ // Inherits other defaults
+ try expect.toBeTrue(std.mem.eql(u8, admin.name, "John Doe"));
+ try expect.toBeTrue(admin.active);
+ }
+
+ test "inactive trait sets active to false" {
+ const inactive = InactiveUserFactory.buildWith(test_alloc, .{});
+
+ try expect.toBeFalse(inactive.active);
+ }
+
+ test "senior trait sets multiple fields" {
+ const senior = SeniorUserFactory.buildWith(test_alloc, .{});
+
+ try expect.equal(senior.age, 65);
+ try expect.toBeTrue(std.mem.eql(u8, senior.role, "senior"));
+ }
+
+ test "traits can be overridden" {
+ const admin = AdminFactory.buildWith(test_alloc, .{
+ .name = "Super Admin",
+ });
+
+ try expect.toBeTrue(std.mem.eql(u8, admin.role, "admin"));
+ try expect.toBeTrue(std.mem.eql(u8, admin.name, "Super Admin"));
+ }
+};
+
+pub const Associations = struct {
+ test "tests:before" {
+ Factory.resetSequences();
+ setupArena();
+ }
+
+ test "tests:after" {
+ teardownArena();
+ }
+
+ test "optional pointer defaults to null" {
+ const user = UserFactory.buildWith(test_alloc, .{});
+ try expect.toBeNull(user.company);
+ }
+
+ test "can override with associated factory" {
+ const company = CompanyFactory.buildPtrWith(test_alloc, .{});
+
+ const user = UserFactory.buildWith(test_alloc, .{
+ .company = company,
+ });
+
+ try expect.notToBeNull(user.company);
+ try expect.toBeTrue(std.mem.eql(u8, user.company.?.name, "Acme Inc"));
+ }
+
+ test "nested associations" {
+ const address = AddressFactory.buildPtrWith(test_alloc, .{});
+
+ const company = CompanyFactory.buildPtrWith(test_alloc, .{
+ .address = address,
+ });
+
+ const user = UserFactory.buildWith(test_alloc, .{
+ .company = company,
+ });
+
+ try expect.notToBeNull(user.company);
+ try expect.notToBeNull(user.company.?.address);
+ try expect.toBeTrue(std.mem.eql(u8, user.company.?.address.?.city, "Springfield"));
+ }
+};
+
+pub const CustomAllocator = struct {
+ test "tests:before" {
+ Factory.resetSequences();
+ }
+
+ test "buildWith uses custom allocator" {
+ var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ const user = UserFactory.buildWith(alloc, .{});
+ try expect.toBeTrue(std.mem.eql(u8, user.name, "John Doe"));
+ }
+
+ test "buildPtrWith uses custom allocator" {
+ var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ const user_ptr = UserFactory.buildPtrWith(alloc, .{});
+ // No need to free - arena handles it
+ try expect.toBeTrue(std.mem.eql(u8, user_ptr.name, "John Doe"));
+ }
+};
+
+pub const PracticalExample = struct {
+ test "tests:before" {
+ Factory.resetSequences();
+ setupArena();
+ }
+
+ test "tests:after" {
+ teardownArena();
+ }
+
+ test "creating test data for user management" {
+ // Create multiple users with different roles
+ const regular_user = UserFactory.buildWith(test_alloc, .{});
+ const admin = AdminFactory.buildWith(test_alloc, .{});
+ const inactive = InactiveUserFactory.buildWith(test_alloc, .{});
+
+ // Create a company with users
+ const company = CompanyFactory.buildPtrWith(test_alloc, .{ .name = "Tech Corp" });
+
+ const employee = UserFactory.buildWith(test_alloc, .{
+ .company = company,
+ .name = "Employee One",
+ });
+
+ // Verify test data
+ try expect.toBeTrue(std.mem.eql(u8, regular_user.role, "user"));
+ try expect.toBeTrue(std.mem.eql(u8, admin.role, "admin"));
+ try expect.toBeFalse(inactive.active);
+ try expect.notToBeNull(employee.company);
+ try expect.toBeTrue(std.mem.eql(u8, employee.company.?.name, "Tech Corp"));
+ }
+
+ test "bulk user creation" {
+ var users: [5]User = undefined;
+
+ for (&users, 0..) |*user, i| {
+ user.* = UserFactory.buildWith(test_alloc, .{
+ .name = if (i == 0) "First User" else "Other User",
+ });
+ }
+
+ // IDs are sequential
+ try expect.equal(users[0].id, 1);
+ try expect.equal(users[4].id, 5);
+
+ // First user has custom name
+ try expect.toBeTrue(std.mem.eql(u8, users[0].name, "First User"));
+ try expect.toBeTrue(std.mem.eql(u8, users[1].name, "Other User"));
+ }
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/factory_zon_example.zon b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/factory_zon_example.zon
new file mode 100644
index 0000000..d05da08
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/factory_zon_example.zon
@@ -0,0 +1,52 @@
+.{
+ // User factory with common defaults
+ .user = .{
+ .id = 0,
+ .name = "John Doe",
+ .email = "john@example.com",
+ .role = "member",
+ .active = true,
+ },
+
+ // Admin user variant - same type, different defaults
+ .admin = .{
+ .id = 0,
+ .name = "Admin User",
+ .email = "admin@example.com",
+ .role = "admin",
+ .active = true,
+ },
+
+ // Product factory
+ .product = .{
+ .id = 0,
+ .name = "Widget",
+ .description = "A useful widget",
+ .price = 29.99,
+ .in_stock = true,
+ .quantity = 100,
+ },
+
+ // Out of stock product variant
+ .out_of_stock_product = .{
+ .id = 0,
+ .name = "Rare Item",
+ .description = "Currently unavailable",
+ .price = 99.99,
+ .in_stock = false,
+ .quantity = 0,
+ },
+
+ // Shape variants for game entities (unions work with anonymous syntax)
+ .circle = .{
+ .shape = .{ .circle = .{ .radius = 25.0 } },
+ .z_index = 10,
+ .visible = true,
+ },
+
+ .rectangle = .{
+ .shape = .{ .rectangle = .{ .width = 100.0, .height = 50.0 } },
+ .z_index = 5,
+ .visible = true,
+ },
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/factory_zon_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/factory_zon_test.zig
new file mode 100644
index 0000000..05b520e
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/factory_zon_test.zig
@@ -0,0 +1,220 @@
+//! Factory.defineFrom() Example
+//!
+//! Demonstrates loading factory definitions from .zon files using Factory.defineFrom().
+//! This pattern offers several benefits:
+//!
+//! - **Separation of concerns**: Test data lives in data files, test logic in test files
+//! - **Reusability**: Share factory definitions across multiple test files
+//! - **Maintainability**: Update test data without touching test code
+//! - **Type safety**: Full compile-time type checking via Zig's comptime system
+//! - **Typo detection**: defineFrom() validates field names at compile time
+//!
+//! Usage:
+//! zig build examples-factory-zon
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+const Factory = zspec.Factory;
+
+test {
+ zspec.runAll(@This());
+}
+
+// =============================================================================
+// Model Definitions
+// =============================================================================
+
+const User = struct {
+ id: u32,
+ name: []const u8,
+ email: []const u8,
+ role: []const u8,
+ active: bool,
+};
+
+const Product = struct {
+ id: u32,
+ name: []const u8,
+ description: []const u8,
+ price: f32,
+ in_stock: bool,
+ quantity: u32,
+};
+
+const Shape = union(enum) {
+ circle: struct { radius: f32 },
+ rectangle: struct { width: f32, height: f32 },
+};
+
+const ShapeVisual = struct {
+ shape: Shape,
+ z_index: u8,
+ visible: bool,
+};
+
+// =============================================================================
+// Load Factory Definitions from .zon File
+// =============================================================================
+
+// Import the .zon file at compile time - no runtime parsing needed!
+const factory_defs = @import("factory_zon_example.zon");
+
+// Define factories using defineFrom() - validates field names and makes intent clear
+const UserFactory = Factory.defineFrom(User, factory_defs.user);
+const AdminFactory = Factory.defineFrom(User, factory_defs.admin);
+const ProductFactory = Factory.defineFrom(Product, factory_defs.product);
+const OutOfStockProductFactory = Factory.defineFrom(Product, factory_defs.out_of_stock_product);
+
+// Union types work seamlessly with .zon anonymous syntax
+const CircleFactory = Factory.defineFrom(ShapeVisual, factory_defs.circle);
+const RectangleFactory = Factory.defineFrom(ShapeVisual, factory_defs.rectangle);
+
+// =============================================================================
+// Example Tests
+// =============================================================================
+
+pub const BASIC_USAGE = struct {
+ test "create user with defaults from .zon" {
+ const user = UserFactory.build(.{});
+
+ try std.testing.expectEqualStrings("John Doe", user.name);
+ try std.testing.expectEqualStrings("john@example.com", user.email);
+ try std.testing.expectEqualStrings("member", user.role);
+ try expect.toBeTrue(user.active);
+ }
+
+ test "create admin with different .zon definition" {
+ const admin = AdminFactory.build(.{});
+
+ try std.testing.expectEqualStrings("Admin User", admin.name);
+ try std.testing.expectEqualStrings("admin@example.com", admin.email);
+ try std.testing.expectEqualStrings("admin", admin.role);
+ }
+
+ test "override .zon defaults at build time" {
+ const user = UserFactory.build(.{
+ .name = "Jane Smith",
+ .email = "jane@example.com",
+ });
+
+ // Overridden values
+ try std.testing.expectEqualStrings("Jane Smith", user.name);
+ try std.testing.expectEqualStrings("jane@example.com", user.email);
+
+ // Defaults from .zon
+ try std.testing.expectEqualStrings("member", user.role);
+ try expect.toBeTrue(user.active);
+ }
+};
+
+pub const PRODUCT_VARIANTS = struct {
+ test "in-stock product from .zon" {
+ const product = ProductFactory.build(.{});
+
+ try std.testing.expectEqualStrings("Widget", product.name);
+ try expect.equal(product.price, 29.99);
+ try expect.toBeTrue(product.in_stock);
+ try expect.equal(product.quantity, 100);
+ }
+
+ test "out-of-stock product variant" {
+ const product = OutOfStockProductFactory.build(.{});
+
+ try std.testing.expectEqualStrings("Rare Item", product.name);
+ try expect.toBeTrue(!product.in_stock);
+ try expect.equal(product.quantity, 0);
+ }
+};
+
+pub const UNION_TYPES = struct {
+ test "circle shape from .zon" {
+ const visual = CircleFactory.build(.{});
+
+ try expect.toBeTrue(visual.visible);
+ try expect.equal(visual.z_index, 10);
+
+ switch (visual.shape) {
+ .circle => |c| try expect.equal(c.radius, 25.0),
+ .rectangle => return error.UnexpectedShape,
+ }
+ }
+
+ test "rectangle shape from .zon" {
+ const visual = RectangleFactory.build(.{});
+
+ try expect.toBeTrue(visual.visible);
+ try expect.equal(visual.z_index, 5);
+
+ switch (visual.shape) {
+ .rectangle => |r| {
+ try expect.equal(r.width, 100.0);
+ try expect.equal(r.height, 50.0);
+ },
+ .circle => return error.UnexpectedShape,
+ }
+ }
+
+ test "override union shape at build time" {
+ // Start with circle, override to rectangle
+ const visual = CircleFactory.build(.{
+ .shape = .{ .rectangle = .{ .width = 200.0, .height = 100.0 } },
+ });
+
+ switch (visual.shape) {
+ .rectangle => |r| {
+ try expect.equal(r.width, 200.0);
+ try expect.equal(r.height, 100.0);
+ },
+ .circle => return error.UnexpectedShape,
+ }
+ }
+};
+
+pub const TRAITS_WITH_ZON = struct {
+ test "apply traits on top of .zon defaults" {
+ // Create an inactive user trait
+ const InactiveUserFactory = UserFactory.trait(.{
+ .active = false,
+ });
+
+ const user = InactiveUserFactory.build(.{});
+
+ // From .zon
+ try std.testing.expectEqualStrings("John Doe", user.name);
+ // From trait
+ try expect.toBeTrue(!user.active);
+ }
+
+ test "chain multiple traits" {
+ const VIPUserFactory = UserFactory
+ .trait(.{ .role = "vip" })
+ .trait(.{ .active = true });
+
+ const user = VIPUserFactory.build(.{});
+
+ try std.testing.expectEqualStrings("vip", user.role);
+ try expect.toBeTrue(user.active);
+ }
+};
+
+pub const EQUIVALENCE = struct {
+ test "defineFrom is equivalent to inline define" {
+ // Inline factory definition
+ const InlineUserFactory = Factory.define(User, .{
+ .id = 0,
+ .name = "John Doe",
+ .email = "john@example.com",
+ .role = "member",
+ .active = true,
+ });
+
+ const from_inline = InlineUserFactory.build(.{});
+ const from_zon = UserFactory.build(.{});
+
+ try std.testing.expectEqualStrings(from_inline.name, from_zon.name);
+ try std.testing.expectEqualStrings(from_inline.email, from_zon.email);
+ try std.testing.expectEqualStrings(from_inline.role, from_zon.role);
+ try expect.equal(from_inline.active, from_zon.active);
+ }
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/fixture_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/fixture_test.zig
new file mode 100644
index 0000000..09faba9
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/fixture_test.zig
@@ -0,0 +1,227 @@
+//! Fixture Module Example
+//!
+//! Demonstrates the Fixture module — a FactoryBot-inspired workflow for static
+//! test data defined in .zon files.
+//!
+//! Key differences from Factory:
+//! - **Fixture**: Static, pre-defined test data. Call `create()` to instantiate.
+//! - **Factory**: Dynamic generation with sequences, lazy values, traits, associations.
+//!
+//! Use Fixture when you want a snapshot of known-good test data.
+//! Use Factory when you need generators that produce unique data each time.
+//!
+//! Usage:
+//! zig build examples-fixture
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+const Fixture = zspec.Fixture;
+
+test {
+ zspec.runAll(@This());
+}
+
+// =============================================================================
+// Model Definitions
+// =============================================================================
+
+const User = struct {
+ id: u32,
+ name: []const u8,
+ email: []const u8,
+};
+
+const Product = struct {
+ id: u32,
+ name: []const u8,
+ price: f32,
+ seller_id: u32,
+};
+
+const Order = struct {
+ id: u32,
+ user_id: u32,
+ product_id: u32,
+ quantity: u32,
+};
+
+const Position = struct { x: f32, y: f32 };
+const Health = struct { current: u32, max: u32 };
+const EnemyKind = enum { slime, goblin, dragon };
+
+const PlayerData = struct {
+ pos: Position,
+ health: Health,
+};
+
+const EnemyData = struct {
+ pos: Position,
+ health: Health,
+ kind: EnemyKind,
+};
+
+// =============================================================================
+// Fixture Definitions
+// =============================================================================
+
+// Load fixture data from a .zon file
+const fixture_data = @import("fixtures.zon");
+
+// Single-struct fixtures: one call to define, then create anywhere
+const UserFixture = Fixture.define(User, fixture_data.user);
+const AdminFixture = Fixture.define(User, fixture_data.admin);
+const ProductFixture = Fixture.define(Product, fixture_data.product);
+
+// Scenario fixture: multiple related structs in one definition
+const CheckoutScenario = struct {
+ user: User,
+ product: Product,
+ order: Order,
+};
+
+const CheckoutFixture = Fixture.define(CheckoutScenario, .{
+ .user = .{ .id = 1, .name = "John Doe", .email = "john@example.com" },
+ .product = .{ .id = 10, .name = "Widget", .price = 29.99, .seller_id = 1 },
+ .order = .{ .id = 100, .user_id = 1, .product_id = 10, .quantity = 2 },
+});
+
+// Battle scenario with arrays and nested structs
+const BattleScenario = struct {
+ player: PlayerData,
+ enemies: [3]EnemyData,
+};
+
+const BattleFixture = Fixture.define(BattleScenario, .{
+ .player = .{
+ .pos = .{ .x = 0.0, .y = 0.0 },
+ .health = .{ .current = 100, .max = 100 },
+ },
+ .enemies = .{
+ .{
+ .pos = .{ .x = 50.0, .y = 30.0 },
+ .health = .{ .current = 20, .max = 20 },
+ .kind = .slime,
+ },
+ .{
+ .pos = .{ .x = 80.0, .y = 60.0 },
+ .health = .{ .current = 50, .max = 50 },
+ .kind = .goblin,
+ },
+ .{
+ .pos = .{ .x = 120.0, .y = 10.0 },
+ .health = .{ .current = 200, .max = 200 },
+ .kind = .dragon,
+ },
+ },
+});
+
+// =============================================================================
+// Example Tests
+// =============================================================================
+
+pub const BASIC_USAGE = struct {
+ test "create a user with defaults" {
+ const user = UserFixture.create(.{});
+
+ try std.testing.expectEqualStrings("John Doe", user.name);
+ try std.testing.expectEqualStrings("john@example.com", user.email);
+ try expect.equal(user.id, 1);
+ }
+
+ test "create an admin with different fixture" {
+ const admin = AdminFixture.create(.{});
+
+ try std.testing.expectEqualStrings("Admin User", admin.name);
+ try expect.equal(admin.id, 2);
+ }
+
+ test "override fields at create time" {
+ const user = UserFixture.create(.{
+ .name = "Jane Smith",
+ .email = "jane@example.com",
+ });
+
+ try std.testing.expectEqualStrings("Jane Smith", user.name);
+ try std.testing.expectEqualStrings("jane@example.com", user.email);
+ // Default from .zon preserved
+ try expect.equal(user.id, 1);
+ }
+};
+
+pub const SCENARIO_USAGE = struct {
+ test "create a complete checkout scenario" {
+ const s = CheckoutFixture.create(.{});
+
+ try expect.equal(s.user.id, 1);
+ try std.testing.expectEqualStrings("Widget", s.product.name);
+ try expect.equal(s.order.quantity, 2);
+ }
+
+ test "verify cross-references in scenario" {
+ const s = CheckoutFixture.create(.{});
+
+ // The order references the user and product by ID
+ try expect.equal(s.order.user_id, s.user.id);
+ try expect.equal(s.order.product_id, s.product.id);
+ try expect.equal(s.product.seller_id, s.user.id);
+ }
+
+ test "override one struct in scenario" {
+ const s = CheckoutFixture.create(.{
+ .order = .{ .id = 200, .user_id = 1, .product_id = 10, .quantity = 10 },
+ });
+
+ try expect.equal(s.order.quantity, 10);
+ try expect.equal(s.order.id, 200);
+ // Other structs unchanged
+ try std.testing.expectEqualStrings("John Doe", s.user.name);
+ }
+};
+
+pub const ARRAYS_AND_NESTED = struct {
+ test "battle scenario with array of enemies" {
+ const battle = BattleFixture.create(.{});
+
+ try expect.equal(battle.player.health.current, 100);
+ try expect.equal(battle.player.pos.x, 0.0);
+
+ try expect.equal(battle.enemies[0].kind, .slime);
+ try expect.equal(battle.enemies[0].health.current, 20);
+
+ try expect.equal(battle.enemies[1].kind, .goblin);
+ try expect.equal(battle.enemies[1].health.current, 50);
+
+ try expect.equal(battle.enemies[2].kind, .dragon);
+ try expect.equal(battle.enemies[2].health.current, 200);
+ }
+
+ test "nested struct positions are correct" {
+ const battle = BattleFixture.create(.{});
+
+ try expect.equal(battle.enemies[0].pos.x, 50.0);
+ try expect.equal(battle.enemies[0].pos.y, 30.0);
+ try expect.equal(battle.enemies[1].pos.x, 80.0);
+ try expect.equal(battle.enemies[2].pos.x, 120.0);
+ }
+};
+
+pub const COMPARISON_WITH_FACTORY = struct {
+ test "Fixture.create vs Factory.build produce equivalent results" {
+ const Factory = zspec.Factory;
+
+ // Factory approach (inline definition)
+ const UserFactory = Factory.define(User, .{
+ .id = 1,
+ .name = "John Doe",
+ .email = "john@example.com",
+ });
+
+ const from_factory = UserFactory.build(.{});
+ const from_fixture = UserFixture.create(.{});
+
+ try std.testing.expectEqualStrings(from_factory.name, from_fixture.name);
+ try std.testing.expectEqualStrings(from_factory.email, from_fixture.email);
+ try expect.equal(from_factory.id, from_fixture.id);
+ }
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/fixtures.zon b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/fixtures.zon
new file mode 100644
index 0000000..161e193
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/fixtures.zon
@@ -0,0 +1,18 @@
+.{
+ .user = .{
+ .id = 1,
+ .name = "John Doe",
+ .email = "john@example.com",
+ },
+ .admin = .{
+ .id = 2,
+ .name = "Admin User",
+ .email = "admin@example.com",
+ },
+ .product = .{
+ .id = 10,
+ .name = "Widget",
+ .price = 29.99,
+ .seller_id = 1,
+ },
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/fsm_integration_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/fsm_integration_test.zig
new file mode 100644
index 0000000..715d944
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/fsm_integration_test.zig
@@ -0,0 +1,489 @@
+//! FSM Integration Example
+//!
+//! Demonstrates how to use ZSpec with zigfsm (https://github.com/cryptocode/zigfsm)
+//! for testing finite state machines.
+//!
+//! Features:
+//! - Factory-based state machine configuration
+//! - Testing state transitions
+//! - Testing event handling
+//! - Verifying state sequences
+//! - Using before/after hooks for FSM setup
+//!
+//! NOTE: This example shows the integration pattern but does not actually
+//! import zigfsm since it's not a dependency. To use this pattern:
+//!
+//! 1. Add zigfsm to your build.zig.zon:
+//! .dependencies = .{
+//! .zspec = .{
+//! .url = "https://github.com/apotema/zspec/archive/refs/heads/main.tar.gz",
+//! .hash = "...",
+//! },
+//! .zigfsm = .{
+//! .url = "https://github.com/cryptocode/zigfsm/archive/refs/heads/main.tar.gz",
+//! .hash = "...",
+//! },
+//! },
+//!
+//! 2. In your build.zig, get modules:
+//! const zspec_dep = b.dependency("zspec", .{ .target = target, .optimize = optimize });
+//! const zspec_mod = zspec_dep.module("zspec");
+//! const zspec_fsm_mod = zspec_dep.module("zspec-fsm");
+//! const zigfsm_dep = b.dependency("zigfsm", .{ .target = target, .optimize = optimize });
+//!
+//! 3. Add to your build.zig test imports:
+//! .imports = &.{
+//! .{ .name = "zspec", .module = zspec_mod },
+//! .{ .name = "zspec-fsm", .module = zspec_fsm_mod },
+//! .{ .name = "zigfsm", .module = zigfsm_dep.module("zigfsm") },
+//! },
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+const Factory = zspec.Factory;
+
+// Import the optional FSM integration module
+const FSM = @import("zspec-fsm");
+
+// Mock zigfsm for demonstration
+// In real usage: const zigfsm = @import("zigfsm");
+const zigfsm = struct {
+ pub fn StateMachine(comptime StateT: type, comptime EventT: type, comptime initial: StateT) type {
+ return struct {
+ state: StateT = initial,
+ transitions: std.ArrayList(Transition),
+ allocator: std.mem.Allocator,
+
+ const Self = @This();
+ pub const State = StateT;
+ pub const Event = EventT;
+
+ const Transition = struct {
+ event: ?Event,
+ from: State,
+ to: State,
+ };
+
+ pub fn init() Self {
+ return .{
+ .transitions = .empty,
+ .allocator = std.testing.allocator,
+ };
+ }
+
+ pub fn deinit(self: *Self) void {
+ self.transitions.deinit(self.allocator);
+ }
+
+ pub fn addTransition(self: *Self, from: StateT, to: StateT) !void {
+ try self.transitions.append(self.allocator, .{ .event = null, .from = from, .to = to });
+ }
+
+ pub fn addEventAndTransition(self: *Self, event: EventT, from: StateT, to: StateT) !void {
+ try self.transitions.append(self.allocator, .{ .event = event, .from = from, .to = to });
+ }
+
+ pub fn do(self: *Self, event: EventT) !void {
+ for (self.transitions.items) |t| {
+ if (t.event) |e| {
+ if (e == event and t.from == self.state) {
+ self.state = t.to;
+ return;
+ }
+ }
+ }
+ return error.InvalidTransition;
+ }
+
+ pub fn transitionTo(self: *Self, to: StateT) !void {
+ for (self.transitions.items) |t| {
+ if (t.from == self.state and t.to == to) {
+ self.state = to;
+ return;
+ }
+ }
+ return error.InvalidTransition;
+ }
+
+ pub fn isCurrently(self: *const Self, state: StateT) bool {
+ return self.state == state;
+ }
+
+ pub fn canTransitionTo(self: *const Self, to: StateT) bool {
+ for (self.transitions.items) |t| {
+ if (t.from == self.state and t.to == to) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ }
+};
+
+test {
+ zspec.runAll(@This());
+}
+
+// =============================================================================
+// State and Event Definitions
+// =============================================================================
+
+const DoorState = enum {
+ closed,
+ open,
+ locked,
+};
+
+const DoorEvent = enum {
+ open_door,
+ close_door,
+ lock_door,
+ unlock_door,
+};
+
+const TrafficLightState = enum {
+ red,
+ yellow,
+ green,
+};
+
+const TrafficLightEvent = enum {
+ timer,
+};
+
+const PlayerState = enum {
+ idle,
+ walking,
+ running,
+ jumping,
+ falling,
+};
+
+const PlayerEvent = enum {
+ walk,
+ run,
+ jump,
+ land,
+ stop,
+};
+
+// =============================================================================
+// Basic FSM Tests
+// =============================================================================
+
+pub const BasicFSMTests = struct {
+ const DoorFSM = zigfsm.StateMachine(DoorState, DoorEvent, .closed);
+ var fsm: DoorFSM = undefined;
+
+ test "tests:before" {
+ fsm = DoorFSM.init();
+ try fsm.addEventAndTransition(.open_door, .closed, .open);
+ try fsm.addEventAndTransition(.close_door, .open, .closed);
+ try fsm.addEventAndTransition(.lock_door, .closed, .locked);
+ try fsm.addEventAndTransition(.unlock_door, .locked, .closed);
+ }
+
+ test "tests:after" {
+ fsm.deinit();
+ }
+
+ test "initial state is closed" {
+ try expect.toBeTrue(fsm.isCurrently(.closed));
+ }
+
+ test "can open door from closed" {
+ try fsm.do(.open_door);
+ try expect.toBeTrue(fsm.isCurrently(.open));
+ }
+
+ test "can close door from open" {
+ try fsm.do(.open_door);
+ try fsm.do(.close_door);
+ try expect.toBeTrue(fsm.isCurrently(.closed));
+ }
+
+ test "can lock and unlock door" {
+ try fsm.do(.lock_door);
+ try expect.toBeTrue(fsm.isCurrently(.locked));
+
+ try fsm.do(.unlock_door);
+ try expect.toBeTrue(fsm.isCurrently(.closed));
+ }
+
+ test "cannot open locked door" {
+ try fsm.do(.lock_door);
+ const result = fsm.do(.open_door);
+ try expect.toBeTrue(std.meta.isError(result));
+ }
+};
+
+// =============================================================================
+// FSM Helper Tests
+// =============================================================================
+
+pub const FSMHelperTests = struct {
+ const TrafficFSM = zigfsm.StateMachine(TrafficLightState, TrafficLightEvent, .red);
+ var fsm: TrafficFSM = undefined;
+
+ test "tests:before" {
+ fsm = TrafficFSM.init();
+ try FSM.addTransitions(TrafficFSM, &fsm, &.{
+ .{ .event = .timer, .from = .red, .to = .green },
+ .{ .event = .timer, .from = .green, .to = .yellow },
+ .{ .event = .timer, .from = .yellow, .to = .red },
+ });
+ }
+
+ test "tests:after" {
+ fsm.deinit();
+ }
+
+ test "addTransitions helper sets up FSM" {
+ try expect.toBeTrue(fsm.isCurrently(.red));
+ try expect.toBeTrue(fsm.canTransitionTo(.green));
+ }
+
+ test "applyEventsAndVerify validates event sequence" {
+ try FSM.applyEventsAndVerify(TrafficFSM, &fsm, &.{
+ .timer,
+ .timer,
+ .timer,
+ }, .red);
+
+ // Should be back to red after full cycle
+ try expect.toBeTrue(fsm.isCurrently(.red));
+ }
+
+ test "expectValidNextStates checks valid transitions" {
+ try FSM.expectValidNextStates(TrafficFSM, &fsm, &.{.green});
+ }
+
+ test "expectInvalidNextStates checks invalid transitions" {
+ try FSM.expectInvalidNextStates(TrafficFSM, &fsm, &.{ .yellow, .red });
+ }
+};
+
+// =============================================================================
+// FSM Builder Pattern Tests
+// =============================================================================
+
+pub const FSMBuilderTests = struct {
+ test "FSM builder creates configured state machine" {
+ const SimpleFSM = zigfsm.StateMachine(enum { a, b, c }, enum { next }, .a);
+ const Builder = FSM.FSMBuilder(SimpleFSM);
+
+ var builder = Builder.init();
+ _ = try builder.withEvent(.next, .a, .b);
+ _ = try builder.withEvent(.next, .b, .c);
+ _ = try builder.withTransition(.c, .a);
+
+ var fsm = builder.build();
+ defer fsm.deinit();
+
+ try expect.toBeTrue(fsm.isCurrently(.a));
+ try fsm.do(.next);
+ try expect.toBeTrue(fsm.isCurrently(.b));
+ }
+};
+
+// =============================================================================
+// Complex Game State Machine Tests
+// =============================================================================
+
+pub const PlayerStateMachineTests = struct {
+ const PlayerFSM = zigfsm.StateMachine(PlayerState, PlayerEvent, .idle);
+ var fsm: PlayerFSM = undefined;
+
+ test "tests:beforeAll" {
+ Factory.resetSequences();
+ }
+
+ test "tests:before" {
+ fsm = PlayerFSM.init();
+ // Idle transitions
+ try fsm.addEventAndTransition(.walk, .idle, .walking);
+ try fsm.addEventAndTransition(.run, .idle, .running);
+ try fsm.addEventAndTransition(.jump, .idle, .jumping);
+
+ // Walking transitions
+ try fsm.addEventAndTransition(.run, .walking, .running);
+ try fsm.addEventAndTransition(.stop, .walking, .idle);
+ try fsm.addEventAndTransition(.jump, .walking, .jumping);
+
+ // Running transitions
+ try fsm.addEventAndTransition(.walk, .running, .walking);
+ try fsm.addEventAndTransition(.stop, .running, .idle);
+ try fsm.addEventAndTransition(.jump, .running, .jumping);
+
+ // Jumping transitions
+ try fsm.addEventAndTransition(.land, .jumping, .idle);
+
+ // Falling transitions (automatic from jumping)
+ try fsm.addTransition(.jumping, .falling);
+ try fsm.addEventAndTransition(.land, .falling, .idle);
+ }
+
+ test "tests:after" {
+ fsm.deinit();
+ }
+
+ test "player starts idle" {
+ try expect.toBeTrue(fsm.isCurrently(.idle));
+ }
+
+ test "player can walk from idle" {
+ try fsm.do(.walk);
+ try expect.toBeTrue(fsm.isCurrently(.walking));
+ }
+
+ test "player can run from idle or walking" {
+ try fsm.do(.walk);
+ try fsm.do(.run);
+ try expect.toBeTrue(fsm.isCurrently(.running));
+ }
+
+ test "player can jump from any ground state" {
+ try fsm.do(.walk);
+ try fsm.do(.jump);
+ try expect.toBeTrue(fsm.isCurrently(.jumping));
+ }
+
+ test "player returns to idle after landing" {
+ try fsm.do(.jump);
+ try fsm.do(.land);
+ try expect.toBeTrue(fsm.isCurrently(.idle));
+ }
+
+ test "complex movement sequence" {
+ // Start idle -> walk -> run -> jump -> land -> idle
+ try FSM.applyEventsAndVerify(PlayerFSM, &fsm, &.{
+ .walk,
+ .run,
+ .jump,
+ .land,
+ }, .idle);
+ }
+
+ test "verify valid next states from idle" {
+ try FSM.expectValidNextStates(PlayerFSM, &fsm, &.{
+ .walking,
+ .running,
+ .jumping,
+ });
+ }
+
+ test "verify invalid transitions from jumping" {
+ try fsm.do(.jump);
+ try FSM.expectInvalidNextStates(PlayerFSM, &fsm, &.{
+ .walking,
+ .running,
+ .jumping,
+ });
+ }
+};
+
+// =============================================================================
+// State Machine Pattern Tests
+// =============================================================================
+
+pub const PatternTests = struct {
+ test "toggle pattern" {
+ const ToggleFSM = zigfsm.StateMachine(enum { on, off }, enum { toggle }, .off);
+ var fsm = ToggleFSM.init();
+ defer fsm.deinit();
+
+ try fsm.addEventAndTransition(.toggle, .off, .on);
+ try fsm.addEventAndTransition(.toggle, .on, .off);
+
+ // Toggle on
+ try fsm.do(.toggle);
+ try expect.toBeTrue(fsm.isCurrently(.on));
+
+ // Toggle off
+ try fsm.do(.toggle);
+ try expect.toBeTrue(fsm.isCurrently(.off));
+
+ // Toggle on again
+ try fsm.do(.toggle);
+ try expect.toBeTrue(fsm.isCurrently(.on));
+ }
+
+ test "linear progression pattern" {
+ const ProgressFSM = zigfsm.StateMachine(
+ enum { step1, step2, step3, done },
+ enum { next },
+ .step1,
+ );
+ var fsm = ProgressFSM.init();
+ defer fsm.deinit();
+
+ try FSM.addTransitions(ProgressFSM, &fsm, &.{
+ .{ .event = .next, .from = .step1, .to = .step2 },
+ .{ .event = .next, .from = .step2, .to = .step3 },
+ .{ .event = .next, .from = .step3, .to = .done },
+ });
+
+ try FSM.applyEventsAndVerify(ProgressFSM, &fsm, &.{ .next, .next, .next }, .done);
+ }
+
+ test "cycle pattern" {
+ const CycleFSM = zigfsm.StateMachine(
+ enum { a, b, c },
+ enum { advance },
+ .a,
+ );
+ var fsm = CycleFSM.init();
+ defer fsm.deinit();
+
+ try fsm.addEventAndTransition(.advance, .a, .b);
+ try fsm.addEventAndTransition(.advance, .b, .c);
+ try fsm.addEventAndTransition(.advance, .c, .a);
+
+ // Complete two full cycles
+ for (0..2) |_| {
+ try fsm.do(.advance);
+ try expect.toBeTrue(fsm.isCurrently(.b));
+ try fsm.do(.advance);
+ try expect.toBeTrue(fsm.isCurrently(.c));
+ try fsm.do(.advance);
+ try expect.toBeTrue(fsm.isCurrently(.a));
+ }
+ }
+};
+
+// =============================================================================
+// Summary
+// =============================================================================
+
+// Key patterns demonstrated:
+//
+// 1. Basic FSM Setup:
+// - Define states and events as enums
+// - Create StateMachine type with initial state
+// - Add transitions and events in before hooks
+// - Clean up in after hooks
+//
+// 2. Testing Transitions:
+// - Use fsm.do(event) to trigger transitions
+// - Use fsm.isCurrently(state) to verify current state
+// - Use fsm.canTransitionTo(state) to check valid transitions
+//
+// 3. FSM Helpers:
+// - FSM.addTransitions() for bulk transition setup
+// - FSM.applyEventsAndVerify() for sequence testing
+// - FSM.expectValidNextStates() for validation
+// - FSM.FSMBuilder for fluent configuration
+//
+// 4. Common Patterns:
+// - Toggle (on/off, open/closed)
+// - Linear progression (step1 -> step2 -> done)
+// - Cycle (a -> b -> c -> a)
+// - Complex state graphs (player movement)
+//
+// 5. Best Practices:
+// - Set up FSM in before hooks for test isolation
+// - Clean up in after hooks
+// - Use helper functions for common assertion patterns
+// - Test invalid transitions to ensure state machine integrity
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/hooks_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/hooks_test.zig
new file mode 100644
index 0000000..4f10604
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/hooks_test.zig
@@ -0,0 +1,144 @@
+//! Hooks Example
+//!
+//! Demonstrates ZSpec's hook system:
+//! - beforeAll: runs once before all tests in a scope
+//! - afterAll: runs once after all tests in a scope
+//! - before: runs before each test in a scope
+//! - after: runs after each test in a scope
+//!
+//! Hooks are scoped - they only apply to tests within their struct.
+//! Parent hooks also apply to nested structs.
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+
+test {
+ zspec.runAll(@This());
+}
+
+// Top-level hooks apply to ALL tests in this file
+var global_setup_count: usize = 0;
+var global_test_count: usize = 0;
+
+test "tests:beforeAll" {
+ global_setup_count = 0;
+ global_test_count = 0;
+ std.debug.print("\n[Global] beforeAll - initializing\n", .{});
+}
+
+test "tests:afterAll" {
+ std.debug.print("[Global] afterAll - ran {d} tests\n", .{global_test_count});
+}
+
+test "tests:before" {
+ global_test_count += 1;
+}
+
+// Database simulation for demonstrating hooks
+pub const Database = struct {
+ var connection: ?*const u8 = null;
+ var query_count: usize = 0;
+
+ // beforeAll: Connect to database once for all tests in this struct
+ test "tests:beforeAll" {
+ connection = @ptrFromInt(0xDEADBEEF); // Simulated connection
+ std.debug.print(" [Database] Connected\n", .{});
+ }
+
+ // afterAll: Disconnect after all tests complete
+ test "tests:afterAll" {
+ connection = null;
+ std.debug.print(" [Database] Disconnected (ran {d} queries)\n", .{query_count});
+ }
+
+ // before: Reset query count before each test
+ test "tests:before" {
+ query_count = 0;
+ }
+
+ // after: Log after each test
+ test "tests:after" {
+ std.debug.print(" (queries this test: {d})\n", .{query_count});
+ }
+
+ test "can execute queries" {
+ try expect.notToBeNull(connection);
+ query_count += 1;
+ try expect.equal(query_count, 1);
+ }
+
+ test "query count resets between tests" {
+ // Thanks to 'before' hook, query_count is 0
+ try expect.equal(query_count, 0);
+ query_count += 3;
+ try expect.equal(query_count, 3);
+ }
+
+ test "connection persists across tests" {
+ // Thanks to 'beforeAll', connection stays open
+ try expect.notToBeNull(connection);
+ }
+};
+
+// Example showing hook inheritance with nested structs
+pub const UserService = struct {
+ var service_initialized: bool = false;
+
+ test "tests:beforeAll" {
+ service_initialized = true;
+ std.debug.print(" [UserService] Initialized\n", .{});
+ }
+
+ test "tests:afterAll" {
+ service_initialized = false;
+ std.debug.print(" [UserService] Shutdown\n", .{});
+ }
+
+ test "service is available" {
+ try expect.toBeTrue(service_initialized);
+ }
+
+ // Nested context - inherits parent hooks
+ pub const Authentication = struct {
+ var auth_enabled: bool = false;
+
+ test "tests:beforeAll" {
+ auth_enabled = true;
+ std.debug.print(" [Authentication] Enabled\n", .{});
+ }
+
+ test "can authenticate users" {
+ // Parent's beforeAll ran first, so service is initialized
+ try expect.toBeTrue(service_initialized);
+ // Our beforeAll ran, so auth is enabled
+ try expect.toBeTrue(auth_enabled);
+ }
+ };
+};
+
+// Example: Using before/after for test isolation
+pub const Counter = struct {
+ var count: i32 = undefined;
+
+ test "tests:before" {
+ count = 0; // Fresh state for each test
+ }
+
+ test "increment" {
+ count += 1;
+ try expect.equal(count, 1);
+ }
+
+ test "increment multiple times" {
+ count += 1;
+ count += 1;
+ count += 1;
+ try expect.equal(count, 3);
+ }
+
+ test "decrement" {
+ count -= 1;
+ try expect.equal(count, -1);
+ }
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/let_memoization_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/let_memoization_test.zig
new file mode 100644
index 0000000..f01ab15
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/let_memoization_test.zig
@@ -0,0 +1,180 @@
+//! Let Memoization Example
+//!
+//! Demonstrates ZSpec's lazy memoization feature (similar to RSpec's `let`):
+//! - Let(T, init_fn): Memoized value computed once per test
+//! - LetAlloc(T, init_fn): Memoized value that requires an allocator
+//!
+//! Key behaviors:
+//! - Value is computed lazily on first .get() call
+//! - Same value is returned on subsequent .get() calls within a test
+//! - Must call .reset() in after hook to clear between tests
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+
+test {
+ zspec.runAll(@This());
+}
+
+// Example: Simple memoized value
+pub const SimpleLet = struct {
+ var computation_count: usize = 0;
+
+ fn computeExpensiveValue() i32 {
+ computation_count += 1;
+ // Simulate expensive computation
+ return 42 * 2;
+ }
+
+ const expensive_value = zspec.Let(i32, computeExpensiveValue);
+
+ test "tests:before" {
+ computation_count = 0;
+ }
+
+ test "tests:after" {
+ expensive_value.reset(); // Important: reset for next test
+ }
+
+ test "value is computed lazily" {
+ // Not computed yet
+ try expect.equal(computation_count, 0);
+
+ // First access triggers computation
+ const val = expensive_value.get();
+ try expect.equal(val, 84);
+ try expect.equal(computation_count, 1);
+ }
+
+ test "value is memoized within a test" {
+ // Call get() multiple times
+ _ = expensive_value.get();
+ _ = expensive_value.get();
+ _ = expensive_value.get();
+
+ // Should only compute once
+ try expect.equal(computation_count, 1);
+ }
+
+ test "value resets between tests" {
+ // Fresh computation for this test (thanks to after hook reset)
+ try expect.equal(computation_count, 0);
+ _ = expensive_value.get();
+ try expect.equal(computation_count, 1);
+ }
+};
+
+// Example: Memoized struct instance
+pub const StructLet = struct {
+ const User = struct {
+ id: u32,
+ name: []const u8,
+ active: bool,
+
+ fn init(id: u32, name: []const u8) User {
+ return .{ .id = id, .name = name, .active = true };
+ }
+ };
+
+ fn createTestUser() User {
+ return User.init(1, "test_user");
+ }
+
+ const test_user = zspec.Let(User, createTestUser);
+
+ test "tests:after" {
+ test_user.reset();
+ }
+
+ test "user has expected properties" {
+ const user = test_user.get();
+ try expect.equal(user.id, 1);
+ try expect.toBeTrue(std.mem.eql(u8, user.name, "test_user"));
+ try expect.toBeTrue(user.active);
+ }
+
+ test "same user instance returned" {
+ const user1 = test_user.get();
+ const user2 = test_user.get();
+ try expect.equal(user1.id, user2.id);
+ try expect.toBeTrue(std.mem.eql(u8, user1.name, user2.name));
+ }
+};
+
+// Example: Using LetAlloc for heap allocations
+pub const AllocLet = struct {
+ const DynamicBuffer = struct {
+ data: []i32,
+ allocator: std.mem.Allocator,
+
+ fn deinit(self: DynamicBuffer) void {
+ self.allocator.free(self.data);
+ }
+ };
+
+ fn createDynamicBuffer(alloc: std.mem.Allocator) DynamicBuffer {
+ const data = alloc.alloc(i32, 5) catch @panic("alloc failed");
+ @memcpy(data, &[_]i32{ 1, 2, 3, 4, 5 });
+ return .{ .data = data, .allocator = alloc };
+ }
+
+ const dynamic_buffer = zspec.LetAlloc(DynamicBuffer, createDynamicBuffer);
+
+ test "tests:after" {
+ // For LetAlloc, free the resource before reset
+ if (dynamic_buffer.get(zspec.allocator).data.len > 0) {
+ dynamic_buffer.get(zspec.allocator).deinit();
+ }
+ dynamic_buffer.reset();
+ }
+
+ test "buffer is initialized with values" {
+ const buf = dynamic_buffer.get(zspec.allocator);
+ try expect.equal(buf.data.len, 5);
+ try expect.equal(buf.data[0], 1);
+ try expect.equal(buf.data[4], 5);
+ }
+
+ test "buffer is memoized" {
+ const buf1 = dynamic_buffer.get(zspec.allocator);
+ const buf2 = dynamic_buffer.get(zspec.allocator);
+ try expect.equal(buf1.data.len, buf2.data.len);
+ }
+};
+
+// Example: Multiple let values in one context
+pub const MultipleLets = struct {
+ fn createConfig() struct { debug: bool, max_retries: u8 } {
+ return .{ .debug = true, .max_retries = 3 };
+ }
+
+ fn createTimeout() u64 {
+ return 5000; // 5 seconds in ms
+ }
+
+ const config = zspec.Let(@TypeOf(createConfig()), createConfig);
+ const timeout = zspec.Let(u64, createTimeout);
+
+ test "tests:after" {
+ config.reset();
+ timeout.reset();
+ }
+
+ test "config values" {
+ const cfg = config.get();
+ try expect.toBeTrue(cfg.debug);
+ try expect.equal(cfg.max_retries, 3);
+ }
+
+ test "timeout value" {
+ try expect.equal(timeout.get(), 5000);
+ }
+
+ test "both values available together" {
+ const cfg = config.get();
+ const t = timeout.get();
+ try expect.toBeTrue(cfg.debug);
+ try expect.equal(t, 5000);
+ }
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/matchers_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/matchers_test.zig
new file mode 100644
index 0000000..228e1bd
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/matchers_test.zig
@@ -0,0 +1,118 @@
+//! Matchers Example
+//!
+//! Demonstrates matchers in ZSpec's expect module with real typed values:
+//! - toBeNull / notToBeNull: Optional value checks
+//! - toHaveLength / toBeEmpty / notToBeEmpty: Length assertions
+//! - Combined assertions on function return values
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+
+test {
+ zspec.runAll(@This());
+}
+
+// Null/Optional Matchers
+pub const Optionals = struct {
+ test "toBeNull with null optional" {
+ const value: ?i32 = null;
+ try expect.toBeNull(value);
+ }
+
+ test "toBeNull with null pointer" {
+ const ptr: ?*u8 = null;
+ try expect.toBeNull(ptr);
+ }
+
+ test "notToBeNull with value" {
+ const value: ?i32 = 42;
+ try expect.notToBeNull(value);
+ }
+
+ test "notToBeNull with pointer" {
+ var x: u8 = 10;
+ const ptr: ?*u8 = &x;
+ try expect.notToBeNull(ptr);
+ }
+};
+
+// Length Matchers
+pub const Lengths = struct {
+ test "toHaveLength with array" {
+ const arr = [_]i32{ 1, 2, 3, 4, 5 };
+ try expect.toHaveLength(&arr, 5);
+ }
+
+ test "toHaveLength with slice" {
+ const slice: []const u8 = "test";
+ try expect.toHaveLength(slice, 4);
+ }
+
+ test "toBeEmpty with empty slice" {
+ const empty: []const u8 = "";
+ try expect.toBeEmpty(empty);
+ }
+
+ test "notToBeEmpty with slice" {
+ const slice: []const u8 = "test";
+ try expect.notToBeEmpty(slice);
+ }
+};
+
+// Combined/Practical Examples
+pub const PracticalExamples = struct {
+ const User = struct {
+ id: u32,
+ name: []const u8,
+ email: ?[]const u8,
+ roles: []const []const u8,
+ };
+
+ fn createUser() User {
+ return .{
+ .id = 1,
+ .name = "John Doe",
+ .email = "john@example.com",
+ .roles = &[_][]const u8{ "admin", "user" },
+ };
+ }
+
+ fn createGuestUser() User {
+ return .{
+ .id = 0,
+ .name = "Guest",
+ .email = null,
+ .roles = &[_][]const u8{},
+ };
+ }
+
+ test "validate regular user" {
+ const user = createUser();
+
+ try expect.toBeGreaterThan(user.id, 0);
+ try expect.notToBeEmpty(user.name);
+ try expect.notToBeNull(user.email);
+ try expect.toContain(user.email.?, "@");
+ try expect.notToBeEmpty(user.roles);
+ try expect.toHaveLength(user.roles, 2);
+ }
+
+ test "validate guest user" {
+ const guest = createGuestUser();
+
+ try expect.equal(guest.id, 0);
+ try expect.toBeTrue(std.mem.eql(u8, guest.name, "Guest"));
+ try expect.toBeNull(guest.email);
+ try expect.toBeEmpty(guest.roles);
+ }
+
+ test "compare users" {
+ const user1 = createUser();
+ const user2 = createGuestUser();
+
+ try expect.notEqual(user1.id, user2.id);
+ try expect.toBeGreaterThan(user1.id, user2.id);
+ try expect.toBeLessThan(user2.roles.len, user1.roles.len);
+ }
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/nested_contexts_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/nested_contexts_test.zig
new file mode 100644
index 0000000..5a1c666
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/examples/nested_contexts_test.zig
@@ -0,0 +1,97 @@
+//! Nested Contexts Example
+//!
+//! Demonstrates nested `pub const` structs for describe/context blocks.
+//! Parent hooks run before child context hooks, enabling layered setup.
+//!
+//! Usage:
+//! zig build examples-nested
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+
+test {
+ zspec.runAll(@This());
+}
+
+// A minimal stack for demonstration
+const Stack = struct {
+ items: [10]i32 = .{0} ** 10,
+ top: usize = 0,
+
+ fn push(self: *Stack, val: i32) void {
+ self.items[self.top] = val;
+ self.top += 1;
+ }
+
+ fn pop(self: *Stack) i32 {
+ self.top -= 1;
+ return self.items[self.top];
+ }
+
+ fn peek(self: *const Stack) i32 {
+ return self.items[self.top - 1];
+ }
+
+ fn isEmpty(self: *const Stack) bool {
+ return self.top == 0;
+ }
+
+ fn size(self: *const Stack) usize {
+ return self.top;
+ }
+};
+
+pub const EMPTY_STACK = struct {
+ var stack: Stack = undefined;
+
+ test "tests:before" {
+ stack = Stack{};
+ }
+
+ test "is empty" {
+ try expect.toBeTrue(stack.isEmpty());
+ }
+
+ test "has size zero" {
+ try expect.equal(stack.size(), 0);
+ }
+
+ pub const AFTER_ONE_PUSH = struct {
+ test "tests:before" {
+ stack.push(42);
+ }
+
+ test "is not empty" {
+ try expect.toBeFalse(stack.isEmpty());
+ }
+
+ test "has size one" {
+ try expect.equal(stack.size(), 1);
+ }
+
+ test "has the pushed value on top" {
+ try expect.equal(stack.peek(), 42);
+ }
+
+ pub const AFTER_SECOND_PUSH = struct {
+ test "tests:before" {
+ stack.push(99);
+ }
+
+ test "has size two" {
+ try expect.equal(stack.size(), 2);
+ }
+
+ test "has the latest value on top" {
+ try expect.equal(stack.peek(), 99);
+ }
+
+ test "pops in LIFO order" {
+ try expect.equal(stack.pop(), 99);
+ try expect.equal(stack.pop(), 42);
+ try expect.toBeTrue(stack.isEmpty());
+ }
+ };
+ };
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/coerce.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/coerce.zig
new file mode 100644
index 0000000..aa90da2
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/coerce.zig
@@ -0,0 +1,168 @@
+//! Shared comptime utilities for struct coercion and validation.
+//!
+//! Used by both Factory and Fixture modules to:
+//! - Validate .zon data fields against target types
+//! - Coerce anonymous structs to named types (recursive)
+//! - Handle union payloads from anonymous struct syntax
+
+const std = @import("std");
+
+/// Validate that all fields in zon_data exist in the target type T (recursive for nested structs).
+/// This catches typos in .zon files at compile time.
+pub fn validateZonFields(comptime T: type, comptime zon_data: anytype) void {
+ const ZonType = @TypeOf(zon_data);
+ const zon_fields = std.meta.fields(ZonType);
+ const target_fields = std.meta.fields(T);
+
+ inline for (zon_fields) |zon_field| {
+ if (!@hasField(T, zon_field.name)) {
+ @compileError("Unknown field '" ++ zon_field.name ++ "' in .zon data. " ++
+ "Type '" ++ @typeName(T) ++ "' has no such field. " ++
+ "Check for typos in your .zon file.");
+ }
+
+ // Find the target field type and recursively validate nested structs
+ inline for (target_fields) |target_field| {
+ if (comptime std.mem.eql(u8, target_field.name, zon_field.name)) {
+ const zon_field_value = @field(zon_data, zon_field.name);
+ const ZonFieldType = @TypeOf(zon_field_value);
+
+ // If both are structs, recursively validate
+ if (@typeInfo(target_field.type) == .@"struct" and @typeInfo(ZonFieldType) == .@"struct") {
+ validateZonFields(target_field.type, zon_field_value);
+ }
+ // If target is union and source is struct, validate the union payload
+ else if (@typeInfo(target_field.type) == .@"union" and @typeInfo(ZonFieldType) == .@"struct") {
+ validateUnionPayload(target_field.type, zon_field_value);
+ }
+ break;
+ }
+ }
+ }
+}
+
+/// Validate that a union payload struct has valid fields
+pub fn validateUnionPayload(comptime UnionType: type, comptime zon_data: anytype) void {
+ const ZonType = @TypeOf(zon_data);
+ const zon_fields = std.meta.fields(ZonType);
+
+ if (zon_fields.len != 1) {
+ @compileError("Union value must have exactly one field matching a union tag");
+ }
+
+ const tag_name = zon_fields[0].name;
+ const union_info = @typeInfo(UnionType).@"union";
+
+ // Find the union field and validate its payload
+ inline for (union_info.fields) |union_field| {
+ if (comptime std.mem.eql(u8, union_field.name, tag_name)) {
+ const payload_value = @field(zon_data, tag_name);
+ const PayloadZonType = @TypeOf(payload_value);
+
+ // If payload is a struct, validate its fields
+ if (@typeInfo(union_field.type) == .@"struct" and @typeInfo(PayloadZonType) == .@"struct") {
+ validateZonFields(union_field.type, payload_value);
+ }
+ return;
+ }
+ }
+
+ @compileError("Unknown union tag '" ++ tag_name ++ "' in .zon data. " ++
+ "Union '" ++ @typeName(UnionType) ++ "' has no such variant.");
+}
+
+/// Coerce an anonymous struct to a union type
+/// e.g., .{ .circle = .{ .radius = 10 } } -> Shape{ .circle = ... }
+pub fn coerceToUnion(comptime UnionType: type, default_value: anytype) UnionType {
+ const DefaultType = @TypeOf(default_value);
+ const default_fields = std.meta.fields(DefaultType);
+
+ // Anonymous struct must have exactly one field
+ if (default_fields.len != 1) {
+ @compileError("Union default value must be a struct with exactly one field matching a union tag");
+ }
+
+ const tag_name = default_fields[0].name;
+ const union_info = @typeInfo(UnionType).@"union";
+
+ // Find the expected payload type for this tag
+ inline for (union_info.fields) |union_field| {
+ if (comptime std.mem.eql(u8, union_field.name, tag_name)) {
+ const PayloadType = union_field.type;
+ const source_payload = @field(default_value, tag_name);
+
+ // Build the correctly-typed payload
+ const typed_payload = buildTypedPayload(PayloadType, source_payload);
+ return @unionInit(UnionType, tag_name, typed_payload);
+ }
+ }
+
+ @compileError("No union field named '" ++ tag_name ++ "' in union type");
+}
+
+/// Build a nested struct from an anonymous source struct (for pointer-to-struct overrides)
+pub fn buildNestedStruct(comptime TargetType: type, source: anytype) TargetType {
+ var result: TargetType = undefined;
+ const SourceType = @TypeOf(source);
+
+ inline for (std.meta.fields(TargetType)) |field| {
+ if (@hasField(SourceType, field.name)) {
+ @field(result, field.name) = @field(source, field.name);
+ } else if (field.default_value_ptr) |default_ptr| {
+ const default_typed: *const field.type = @ptrCast(@alignCast(default_ptr));
+ @field(result, field.name) = default_typed.*;
+ } else {
+ @compileError("Missing field in nested override: " ++ field.name);
+ }
+ }
+
+ return result;
+}
+
+/// Build a typed value from an anonymous source value (recursive for nested structs)
+pub fn buildTypedPayload(comptime TargetType: type, source: anytype) TargetType {
+ const SourceType = @TypeOf(source);
+
+ // If already the right type, return directly
+ if (SourceType == TargetType) {
+ return source;
+ }
+
+ // If both are structs, copy fields (with support for default values and recursive coercion)
+ if (@typeInfo(TargetType) == .@"struct" and @typeInfo(SourceType) == .@"struct") {
+ var result: TargetType = undefined;
+ inline for (std.meta.fields(TargetType)) |field| {
+ if (@hasField(SourceType, field.name)) {
+ const source_value = @field(source, field.name);
+ const SourceFieldType = @TypeOf(source_value);
+
+ // If field types match, assign directly
+ if (SourceFieldType == field.type) {
+ @field(result, field.name) = source_value;
+ }
+ // If both are structs but different types, recursively coerce
+ else if (@typeInfo(field.type) == .@"struct" and @typeInfo(SourceFieldType) == .@"struct") {
+ @field(result, field.name) = buildTypedPayload(field.type, source_value);
+ }
+ // If target is union and source is struct, coerce to union
+ else if (@typeInfo(field.type) == .@"union" and @typeInfo(SourceFieldType) == .@"struct") {
+ @field(result, field.name) = coerceToUnion(field.type, source_value);
+ }
+ // Otherwise try direct coercion
+ else {
+ @field(result, field.name) = source_value;
+ }
+ } else if (field.default_value_ptr) |default_ptr| {
+ // Use the field's default value if not provided
+ const default_typed: *const field.type = @ptrCast(@alignCast(default_ptr));
+ @field(result, field.name) = default_typed.*;
+ } else {
+ @compileError("Missing field '" ++ field.name ++ "' in struct (no default value)");
+ }
+ }
+ return result;
+ }
+
+ // For non-struct types, try explicit coercion
+ return @as(TargetType, source);
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/factory.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/factory.zig
new file mode 100644
index 0000000..acb434b
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/factory.zig
@@ -0,0 +1,760 @@
+//! ZSpec Factory - FactoryBot-like test data generation for Zig
+//!
+//! Provides:
+//! - Factory.define() - Define factories with default values
+//! - Factory.sequence() - Auto-incrementing values
+//! - Factory.sequenceFmt() - Formatted sequence strings
+//! - Factory.lazy() - Computed values
+//! - Factory.assoc() - Nested factory associations
+//! - .trait() - Predefined variants
+//! - .build() / .buildPtr() - Create instances
+
+const std = @import("std");
+const coerce = @import("coerce.zig");
+
+/// Global sequence counters - reset with resetSequences()
+/// Using a simple array-based approach for simplicity and to avoid hashmap issues
+const MAX_SEQUENCES = 256;
+var sequence_values: [MAX_SEQUENCES]u64 = [_]u64{0} ** MAX_SEQUENCES;
+
+/// Reset all sequence counters to 0
+pub fn resetSequences() void {
+ sequence_values = [_]u64{0} ** MAX_SEQUENCES;
+}
+
+fn getNextSequence(id: usize) u64 {
+ const index = id % MAX_SEQUENCES;
+ sequence_values[index] += 1;
+ return sequence_values[index];
+}
+
+/// Marker type for sequence fields
+pub fn SequenceType(comptime T: type) type {
+ return struct {
+ pub const sequence_type = T;
+ pub const is_sequence = true;
+ };
+}
+
+/// Marker type for formatted sequence fields
+pub fn SequenceFmtType(comptime fmt: []const u8) type {
+ return struct {
+ pub const format_string = fmt;
+ pub const is_sequence_fmt = true;
+ };
+}
+
+/// Marker type for lazy/computed fields
+pub fn Lazy(comptime T: type, comptime func: fn () T) type {
+ return struct {
+ pub const lazy_type = T;
+ pub const compute = func;
+ pub const is_lazy = true;
+ };
+}
+
+/// Marker type for lazy fields with allocator
+pub fn LazyAlloc(comptime T: type, comptime func: fn (std.mem.Allocator) T) type {
+ return struct {
+ pub const lazy_type = T;
+ pub const computeAlloc = func;
+ pub const is_lazy_alloc = true;
+ };
+}
+
+/// Marker type for associations
+pub fn Assoc(comptime FactoryType: type) type {
+ return struct {
+ pub const factory = FactoryType;
+ pub const is_assoc = true;
+ };
+}
+
+/// Create a sequence marker for auto-incrementing numeric values.
+pub fn sequence(comptime T: type) SequenceType(T) {
+ return .{};
+}
+
+/// Create a formatted sequence marker for strings like "user{d}@example.com".
+pub fn sequenceFmt(comptime fmt: []const u8) SequenceFmtType(fmt) {
+ return .{};
+}
+
+/// Create a lazy marker for computed values
+pub fn lazy(comptime func: anytype) Lazy(@typeInfo(@TypeOf(func)).@"fn".return_type.?, func) {
+ return .{};
+}
+
+/// Create a lazy marker for computed values that need an allocator
+pub fn lazyAlloc(comptime func: anytype) LazyAlloc(@typeInfo(@TypeOf(func)).@"fn".return_type.?, func) {
+ return .{};
+}
+
+/// Create an association marker for nested factories
+pub fn assoc(comptime FactoryType: type) Assoc(FactoryType) {
+ return .{};
+}
+
+/// Define a factory for a given type with default values
+pub fn define(comptime T: type, comptime defaults: anytype) type {
+ return FactoryImpl(T, defaults, 0);
+}
+
+/// Define a factory from comptime data (e.g., imported from a .zon file)
+///
+/// This is a convenience wrapper around `define()` that validates unknown fields
+/// and makes the intent clear when loading factory definitions from external .zon files.
+///
+/// Unlike `define()`, this function will produce a compile error if the .zon data
+/// contains fields that don't exist in the target type T, catching typos early.
+///
+/// Example usage:
+/// ```zig
+/// const factory_defs = @import("test_factories.zon");
+/// pub const UserFactory = Factory.defineFrom(User, factory_defs.user);
+/// pub const ProductFactory = Factory.defineFrom(Product, factory_defs.product);
+/// ```
+///
+/// The .zon file would contain:
+/// ```zig
+/// .{
+/// .user = .{ .name = "John", .email = "john@example.com", .age = 25 },
+/// .product = .{ .name = "Widget", .price = 9.99, .in_stock = true },
+/// }
+/// ```
+///
+/// Note: .zon files contain static comptime data only. For dynamic features like
+/// sequences or lazy values, use `define()` directly or apply them via traits.
+pub fn defineFrom(comptime T: type, comptime zon_data: anytype) type {
+ // Validate that all fields in zon_data exist in T (catches typos in .zon files)
+ validateZonFields(T, zon_data);
+ return define(T, zon_data);
+}
+
+// Shared comptime utilities (extracted to coerce.zig)
+const validateZonFields = coerce.validateZonFields;
+const coerceToUnion = coerce.coerceToUnion;
+const buildNestedStruct = coerce.buildNestedStruct;
+const buildTypedPayload = coerce.buildTypedPayload;
+
+fn FactoryImpl(comptime T: type, comptime defaults: anytype, comptime depth: usize) type {
+ if (depth > 3) {
+ @compileError("Factory associations cannot be nested more than 3 levels deep");
+ }
+
+ return struct {
+ const Self = @This();
+ pub const Target = T;
+ pub const default_values = defaults;
+ pub const nesting_depth = depth;
+
+ /// Build an instance using std.testing.allocator
+ pub fn build(overrides: anytype) T {
+ return buildWith(std.testing.allocator, overrides);
+ }
+
+ /// Build a pointer instance using std.testing.allocator
+ pub fn buildPtr(overrides: anytype) *T {
+ return buildPtrWith(std.testing.allocator, overrides);
+ }
+
+ /// Build an instance using a custom allocator
+ pub fn buildWith(alloc: std.mem.Allocator, overrides: anytype) T {
+ return buildInternal(alloc, overrides);
+ }
+
+ /// Build a pointer instance using a custom allocator
+ pub fn buildPtrWith(alloc: std.mem.Allocator, overrides: anytype) *T {
+ const ptr = alloc.create(T) catch @panic("factory allocation failed");
+ ptr.* = buildInternal(alloc, overrides);
+ return ptr;
+ }
+
+ fn buildInternal(alloc: std.mem.Allocator, overrides: anytype) T {
+ var result: T = undefined;
+ const target_fields = std.meta.fields(T);
+
+ inline for (target_fields) |field| {
+ const field_name = field.name;
+ @field(result, field_name) = resolveField(field.type, field_name, alloc, overrides);
+ }
+
+ return result;
+ }
+
+ fn resolveField(comptime FieldType: type, comptime field_name: []const u8, alloc: std.mem.Allocator, overrides: anytype) FieldType {
+ const OverridesType = @TypeOf(overrides);
+
+ // Check if field is overridden
+ if (OverridesType != @TypeOf(.{})) {
+ if (@hasField(OverridesType, field_name)) {
+ const override_value = @field(overrides, field_name);
+ return processOverride(FieldType, override_value, alloc);
+ }
+ }
+
+ // Use default value
+ if (@hasField(@TypeOf(defaults), field_name)) {
+ const default_value = @field(defaults, field_name);
+ return resolveDefaultWithField(FieldType, field_name, default_value, alloc);
+ }
+
+ // Handle optional pointer - default to null
+ if (comptime isOptionalPointer(FieldType)) {
+ return null;
+ }
+
+ @compileError("No default value provided for field: " ++ field_name);
+ }
+
+ fn processOverride(comptime FieldType: type, override_value: anytype, alloc: std.mem.Allocator) FieldType {
+ const OverrideType = @TypeOf(override_value);
+
+ // Direct value assignment
+ if (OverrideType == FieldType) {
+ return override_value;
+ }
+
+ // Handle anonymous struct to union coercion
+ if (@typeInfo(FieldType) == .@"union" and @typeInfo(OverrideType) == .@"struct") {
+ return coerceToUnion(FieldType, override_value);
+ }
+
+ // Handle struct overrides for nested types (pointer-to-struct)
+ if (@typeInfo(OverrideType) == .@"struct" and @typeInfo(FieldType) == .pointer) {
+ const ChildType = @typeInfo(FieldType).pointer.child;
+ if (@typeInfo(ChildType) == .@"struct") {
+ const ptr = alloc.create(ChildType) catch @panic("factory allocation failed");
+ ptr.* = buildNestedStruct(ChildType, override_value);
+ return ptr;
+ }
+ }
+
+ // Handle anonymous struct to named struct coercion
+ // e.g., .build(.{ .tint = .{ .r = 255, ... } }) -> Color{ .r = 255, ... }
+ if (@typeInfo(FieldType) == .@"struct" and @typeInfo(OverrideType) == .@"struct") {
+ return buildTypedPayload(FieldType, override_value);
+ }
+
+ // Coerce compatible types
+ return @as(FieldType, override_value);
+ }
+
+ fn computeFieldHash(comptime field_name: []const u8) usize {
+ // Create a unique ID based on type name and field name
+ var hash: usize = 0;
+ for (@typeName(T)) |c| {
+ hash = hash *% 31 +% c;
+ }
+ for (field_name) |c| {
+ hash = hash *% 31 +% c;
+ }
+ return hash;
+ }
+
+ fn resolveDefaultWithField(comptime FieldType: type, comptime field_name: []const u8, default_value: anytype, alloc: std.mem.Allocator) FieldType {
+ const DefaultType = @TypeOf(default_value);
+
+ // Handle sequence markers
+ if (@typeInfo(DefaultType) == .@"struct" and @hasDecl(DefaultType, "is_sequence")) {
+ const seq_id = comptime computeFieldHash(field_name);
+ const seq_num = getNextSequence(seq_id);
+ return @as(FieldType, @intCast(seq_num));
+ }
+
+ // Handle sequence format markers
+ if (@typeInfo(DefaultType) == .@"struct" and @hasDecl(DefaultType, "is_sequence_fmt")) {
+ const seq_id = comptime computeFieldHash(field_name);
+ const seq_num = getNextSequence(seq_id);
+ return std.fmt.allocPrint(alloc, DefaultType.format_string, .{seq_num}) catch @panic("sequence format failed");
+ }
+
+ // Handle lazy markers
+ if (@typeInfo(DefaultType) == .@"struct" and @hasDecl(DefaultType, "is_lazy")) {
+ return DefaultType.compute();
+ }
+
+ // Handle lazy alloc markers
+ if (@typeInfo(DefaultType) == .@"struct" and @hasDecl(DefaultType, "is_lazy_alloc")) {
+ return DefaultType.computeAlloc(alloc);
+ }
+
+ // Handle association markers
+ if (@typeInfo(DefaultType) == .@"struct" and @hasDecl(DefaultType, "is_assoc")) {
+ const AssocFactory = DefaultType.factory;
+ return AssocFactory.buildPtrWith(alloc, .{});
+ }
+
+ // Handle null for optional pointers
+ if (DefaultType == @TypeOf(null) and comptime isOptionalPointer(FieldType)) {
+ return null;
+ }
+
+ // Direct value
+ if (DefaultType == FieldType) {
+ return default_value;
+ }
+
+ // Handle anonymous struct to union coercion
+ // e.g., .{ .circle = .{ .radius = 10 } } -> Shape{ .circle = ... }
+ if (@typeInfo(FieldType) == .@"union" and @typeInfo(DefaultType) == .@"struct") {
+ return coerceToUnion(FieldType, default_value);
+ }
+
+ // Handle anonymous struct to named struct coercion
+ // e.g., .{ .r = 255, .g = 255, .b = 255, .a = 255 } -> Color{ .r = 255, ... }
+ if (@typeInfo(FieldType) == .@"struct" and @typeInfo(DefaultType) == .@"struct") {
+ return buildTypedPayload(FieldType, default_value);
+ }
+
+ // Try coercion
+ return @as(FieldType, default_value);
+ }
+
+ fn isOptionalPointer(comptime FieldType: type) bool {
+ if (@typeInfo(FieldType) != .optional) return false;
+ const child = @typeInfo(FieldType).optional.child;
+ return @typeInfo(child) == .pointer;
+ }
+
+ /// Create a new factory with additional/overridden defaults (trait)
+ pub fn trait(comptime trait_values: anytype) type {
+ return TraitFactoryImpl(T, defaults, trait_values, depth);
+ }
+ };
+}
+
+/// Factory implementation for traits - stores both base defaults and trait overrides
+fn TraitFactoryImpl(comptime T: type, comptime base_defaults: anytype, comptime trait_overrides: anytype, comptime depth: usize) type {
+ if (depth > 3) {
+ @compileError("Factory associations cannot be nested more than 3 levels deep");
+ }
+
+ return struct {
+ const Self = @This();
+ pub const Target = T;
+ pub const nesting_depth = depth;
+
+ /// Build an instance using std.testing.allocator
+ pub fn build(overrides: anytype) T {
+ return buildWith(std.testing.allocator, overrides);
+ }
+
+ /// Build a pointer instance using std.testing.allocator
+ pub fn buildPtr(overrides: anytype) *T {
+ return buildPtrWith(std.testing.allocator, overrides);
+ }
+
+ /// Build an instance using a custom allocator
+ pub fn buildWith(alloc: std.mem.Allocator, overrides: anytype) T {
+ return buildInternal(alloc, overrides);
+ }
+
+ /// Build a pointer instance using a custom allocator
+ pub fn buildPtrWith(alloc: std.mem.Allocator, overrides: anytype) *T {
+ const ptr = alloc.create(T) catch @panic("factory allocation failed");
+ ptr.* = buildInternal(alloc, overrides);
+ return ptr;
+ }
+
+ fn buildInternal(alloc: std.mem.Allocator, overrides: anytype) T {
+ var result: T = undefined;
+ const target_fields = std.meta.fields(T);
+
+ inline for (target_fields) |field| {
+ const field_name = field.name;
+ @field(result, field_name) = resolveField(field.type, field_name, alloc, overrides);
+ }
+
+ return result;
+ }
+
+ fn resolveField(comptime FieldType: type, comptime field_name: []const u8, alloc: std.mem.Allocator, overrides: anytype) FieldType {
+ const OverridesType = @TypeOf(overrides);
+
+ // Check if field is overridden at call site
+ if (OverridesType != @TypeOf(.{})) {
+ if (@hasField(OverridesType, field_name)) {
+ const override_value = @field(overrides, field_name);
+ return processOverride(FieldType, override_value, alloc);
+ }
+ }
+
+ // Check if field is in trait overrides
+ if (@hasField(@TypeOf(trait_overrides), field_name)) {
+ const trait_value = @field(trait_overrides, field_name);
+ return resolveDefaultWithField(FieldType, field_name, trait_value, alloc);
+ }
+
+ // Use base default value
+ if (@hasField(@TypeOf(base_defaults), field_name)) {
+ const default_value = @field(base_defaults, field_name);
+ return resolveDefaultWithField(FieldType, field_name, default_value, alloc);
+ }
+
+ // Handle optional pointer - default to null
+ if (comptime isOptionalPointer(FieldType)) {
+ return null;
+ }
+
+ @compileError("No default value provided for field: " ++ field_name);
+ }
+
+ fn processOverride(comptime FieldType: type, override_value: anytype, alloc: std.mem.Allocator) FieldType {
+ const OverrideType = @TypeOf(override_value);
+
+ // Direct value assignment
+ if (OverrideType == FieldType) {
+ return override_value;
+ }
+
+ // Handle anonymous struct to union coercion
+ if (@typeInfo(FieldType) == .@"union" and @typeInfo(OverrideType) == .@"struct") {
+ return coerceToUnion(FieldType, override_value);
+ }
+
+ // Handle struct overrides for nested types (pointer-to-struct)
+ if (@typeInfo(OverrideType) == .@"struct" and @typeInfo(FieldType) == .pointer) {
+ const ChildType = @typeInfo(FieldType).pointer.child;
+ if (@typeInfo(ChildType) == .@"struct") {
+ const ptr = alloc.create(ChildType) catch @panic("factory allocation failed");
+ ptr.* = buildNestedStruct(ChildType, override_value);
+ return ptr;
+ }
+ }
+
+ // Handle anonymous struct to named struct coercion
+ // e.g., .build(.{ .tint = .{ .r = 255, ... } }) -> Color{ .r = 255, ... }
+ if (@typeInfo(FieldType) == .@"struct" and @typeInfo(OverrideType) == .@"struct") {
+ return buildTypedPayload(FieldType, override_value);
+ }
+
+ // Coerce compatible types
+ return @as(FieldType, override_value);
+ }
+
+ fn computeFieldHash(comptime field_name: []const u8) usize {
+ var hash: usize = 0;
+ for (@typeName(T)) |c| {
+ hash = hash *% 31 +% c;
+ }
+ for (field_name) |c| {
+ hash = hash *% 31 +% c;
+ }
+ return hash;
+ }
+
+ fn resolveDefaultWithField(comptime FieldType: type, comptime field_name: []const u8, default_value: anytype, alloc: std.mem.Allocator) FieldType {
+ const DefaultType = @TypeOf(default_value);
+
+ // Handle sequence markers
+ if (@typeInfo(DefaultType) == .@"struct" and @hasDecl(DefaultType, "is_sequence")) {
+ const seq_id = comptime computeFieldHash(field_name);
+ const seq_num = getNextSequence(seq_id);
+ return @as(FieldType, @intCast(seq_num));
+ }
+
+ // Handle sequence format markers
+ if (@typeInfo(DefaultType) == .@"struct" and @hasDecl(DefaultType, "is_sequence_fmt")) {
+ const seq_id = comptime computeFieldHash(field_name);
+ const seq_num = getNextSequence(seq_id);
+ return std.fmt.allocPrint(alloc, DefaultType.format_string, .{seq_num}) catch @panic("sequence format failed");
+ }
+
+ // Handle lazy markers
+ if (@typeInfo(DefaultType) == .@"struct" and @hasDecl(DefaultType, "is_lazy")) {
+ return DefaultType.compute();
+ }
+
+ // Handle lazy alloc markers
+ if (@typeInfo(DefaultType) == .@"struct" and @hasDecl(DefaultType, "is_lazy_alloc")) {
+ return DefaultType.computeAlloc(alloc);
+ }
+
+ // Handle association markers
+ if (@typeInfo(DefaultType) == .@"struct" and @hasDecl(DefaultType, "is_assoc")) {
+ const AssocFactory = DefaultType.factory;
+ return AssocFactory.buildPtrWith(alloc, .{});
+ }
+
+ // Handle null for optional pointers
+ if (DefaultType == @TypeOf(null) and comptime isOptionalPointer(FieldType)) {
+ return null;
+ }
+
+ // Direct value
+ if (DefaultType == FieldType) {
+ return default_value;
+ }
+
+ // Handle anonymous struct to union coercion
+ // e.g., .{ .circle = .{ .radius = 10 } } -> Shape{ .circle = ... }
+ if (@typeInfo(FieldType) == .@"union" and @typeInfo(DefaultType) == .@"struct") {
+ return coerceToUnion(FieldType, default_value);
+ }
+
+ // Handle anonymous struct to named struct coercion
+ // e.g., .{ .r = 255, .g = 255, .b = 255, .a = 255 } -> Color{ .r = 255, ... }
+ if (@typeInfo(FieldType) == .@"struct" and @typeInfo(DefaultType) == .@"struct") {
+ return buildTypedPayload(FieldType, default_value);
+ }
+
+ // Try coercion
+ return @as(FieldType, default_value);
+ }
+
+ fn isOptionalPointer(comptime FieldType: type) bool {
+ if (@typeInfo(FieldType) != .optional) return false;
+ const child = @typeInfo(FieldType).optional.child;
+ return @typeInfo(child) == .pointer;
+ }
+
+ /// Create a new factory with additional/overridden defaults (trait)
+ pub fn trait(comptime new_trait_values: anytype) type {
+ // Chain traits by creating a new trait factory with combined overrides
+ return TraitFactoryImpl(T, base_defaults, mergeTrait(trait_overrides, new_trait_values), depth);
+ }
+
+ fn mergeTrait(comptime base: anytype, comptime overlay: anytype) MergedTraitType(base, overlay) {
+ const OverlayType = @TypeOf(overlay);
+ var result: MergedTraitType(base, overlay) = undefined;
+ // Only copy base fields that are NOT overridden by overlay (to avoid type mismatch)
+ inline for (std.meta.fields(@TypeOf(base))) |field| {
+ if (!@hasField(OverlayType, field.name)) {
+ @field(result, field.name) = @field(base, field.name);
+ }
+ }
+ // Copy all overlay fields
+ inline for (std.meta.fields(OverlayType)) |field| {
+ @field(result, field.name) = @field(overlay, field.name);
+ }
+ return result;
+ }
+
+ fn MergedTraitType(comptime base: anytype, comptime overlay: anytype) type {
+ const BaseType = @TypeOf(base);
+ const OverlayType = @TypeOf(overlay);
+ const base_fields = std.meta.fields(BaseType);
+ const overlay_fields = std.meta.fields(OverlayType);
+
+ const total = base_fields.len + overlay_fields.len;
+ var names: [total][:0]const u8 = undefined;
+ var types: [total]type = undefined;
+ var attrs: [total]std.builtin.Type.StructField.Attributes = undefined;
+ var count: usize = 0;
+
+ // Add base fields (that are not in overlay)
+ inline for (base_fields) |field| {
+ if (!@hasField(OverlayType, field.name)) {
+ names[count] = field.name;
+ types[count] = field.type;
+ attrs[count] = .{
+ .@"comptime" = field.is_comptime,
+ .@"align" = field.alignment,
+ .default_value_ptr = field.default_value_ptr,
+ };
+ count += 1;
+ }
+ }
+
+ // Add all overlay fields
+ inline for (overlay_fields) |field| {
+ names[count] = field.name;
+ types[count] = field.type;
+ attrs[count] = .{
+ .@"comptime" = field.is_comptime,
+ .@"align" = field.alignment,
+ .default_value_ptr = field.default_value_ptr,
+ };
+ count += 1;
+ }
+
+ return @Struct(.auto, null, names[0..count], types[0..count], attrs[0..count]);
+ }
+ };
+}
+
+// Tests
+test "basic factory" {
+ const User = struct {
+ name: []const u8,
+ age: u8,
+ active: bool,
+ };
+
+ const UserFactory = define(User, .{
+ .name = "John Doe",
+ .age = 25,
+ .active = true,
+ });
+
+ const user = UserFactory.build(.{});
+ try std.testing.expectEqualStrings("John Doe", user.name);
+ try std.testing.expectEqual(@as(u8, 25), user.age);
+ try std.testing.expect(user.active);
+}
+
+test "factory with overrides" {
+ const User = struct {
+ name: []const u8,
+ age: u8,
+ };
+
+ const UserFactory = define(User, .{
+ .name = "John",
+ .age = 25,
+ });
+
+ const user = UserFactory.build(.{ .name = "Jane", .age = 30 });
+ try std.testing.expectEqualStrings("Jane", user.name);
+ try std.testing.expectEqual(@as(u8, 30), user.age);
+}
+
+test "factory sequence" {
+ resetSequences();
+
+ const User = struct {
+ id: u32,
+ name: []const u8,
+ };
+
+ const UserFactory = define(User, .{
+ .id = sequence(u32),
+ .name = "User",
+ });
+
+ const user1 = UserFactory.build(.{});
+ const user2 = UserFactory.build(.{});
+ const user3 = UserFactory.build(.{});
+
+ try std.testing.expectEqual(@as(u32, 1), user1.id);
+ try std.testing.expectEqual(@as(u32, 2), user2.id);
+ try std.testing.expectEqual(@as(u32, 3), user3.id);
+}
+
+test "factory sequenceFmt" {
+ resetSequences();
+
+ const User = struct {
+ email: []const u8,
+ };
+
+ const UserFactory = define(User, .{
+ .email = sequenceFmt("user{d}@example.com"),
+ });
+
+ // Use arena allocator since sequenceFmt allocates strings
+ var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ defer arena.deinit();
+
+ const user1 = UserFactory.buildWith(arena.allocator(), .{});
+ const user2 = UserFactory.buildWith(arena.allocator(), .{});
+
+ try std.testing.expectEqualStrings("user1@example.com", user1.email);
+ try std.testing.expectEqualStrings("user2@example.com", user2.email);
+}
+
+test "factory trait" {
+ const User = struct {
+ name: []const u8,
+ role: []const u8,
+ active: bool,
+ };
+
+ const UserFactory = define(User, .{
+ .name = "John",
+ .role = "user",
+ .active = true,
+ });
+
+ const AdminFactory = UserFactory.trait(.{
+ .role = "admin",
+ });
+
+ const user = UserFactory.build(.{});
+ const admin = AdminFactory.build(.{});
+
+ try std.testing.expectEqualStrings("user", user.role);
+ try std.testing.expectEqualStrings("admin", admin.role);
+ try std.testing.expectEqualStrings("John", admin.name); // inherited
+}
+
+test "factory buildPtr" {
+ const User = struct {
+ name: []const u8,
+ };
+
+ const UserFactory = define(User, .{
+ .name = "John",
+ });
+
+ const user_ptr = UserFactory.buildPtr(.{});
+ defer std.testing.allocator.destroy(user_ptr);
+
+ try std.testing.expectEqualStrings("John", user_ptr.name);
+}
+
+test "factory optional pointer defaults to null" {
+ const Company = struct {
+ name: []const u8,
+ };
+
+ const User = struct {
+ name: []const u8,
+ company: ?*Company,
+ };
+
+ const UserFactory = define(User, .{
+ .name = "John",
+ .company = null,
+ });
+
+ const user = UserFactory.build(.{});
+ try std.testing.expectEqualStrings("John", user.name);
+ try std.testing.expect(user.company == null);
+}
+
+test "factory lazy value" {
+ var counter: u32 = 0;
+
+ const Item = struct {
+ value: u32,
+ };
+
+ const getCounter = struct {
+ fn get() u32 {
+ return 42;
+ }
+ }.get;
+
+ const ItemFactory = define(Item, .{
+ .value = lazy(getCounter),
+ });
+
+ _ = &counter;
+
+ const item = ItemFactory.build(.{});
+ try std.testing.expectEqual(@as(u32, 42), item.value);
+}
+
+test "resetSequences resets counters" {
+ resetSequences();
+
+ const Item = struct {
+ id: u32,
+ };
+
+ const ItemFactory = define(Item, .{
+ .id = sequence(u32),
+ });
+
+ _ = ItemFactory.build(.{});
+ _ = ItemFactory.build(.{});
+ const before_reset = ItemFactory.build(.{});
+ try std.testing.expectEqual(@as(u32, 3), before_reset.id);
+
+ resetSequences();
+
+ const after_reset = ItemFactory.build(.{});
+ try std.testing.expectEqual(@as(u32, 1), after_reset.id);
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/fixture.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/fixture.zig
new file mode 100644
index 0000000..4a86a18
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/fixture.zig
@@ -0,0 +1,357 @@
+//! ZSpec Fixture - Static test data instantiation from .zon files
+//!
+//! Provides a FactoryBot-inspired workflow for static test data:
+//! - Define fixtures once in .zon files
+//! - Call `create()` anywhere in tests with optional overrides
+//!
+//! Unlike Factory (which handles dynamic generation with sequences, lazy values,
+//! and traits), Fixture is designed for static, pre-defined test data — complete
+//! snapshots of known-good state.
+//!
+//! Supports:
+//! - Single struct fixtures: `Fixture.define(User, @import("user.zon"))`
+//! - Scenario fixtures: `Fixture.define(CheckoutScenario, @import("checkout.zon"))`
+//! - Fixed-size arrays: `[3]Product` fields populated from .zon tuples
+//! - Nested structs and unions: recursive coercion from anonymous structs
+
+const std = @import("std");
+const coerce = @import("coerce.zig");
+
+/// Define a fixture for a given type with .zon data defaults.
+///
+/// Returns a type with a `create(overrides)` method.
+/// All fields in `zon_data` are validated against `T` at compile time.
+///
+/// Example:
+/// ```zig
+/// const UserFixture = Fixture.define(User, @import("fixtures/user.zon"));
+/// const user = UserFixture.create(.{});
+/// const custom = UserFixture.create(.{ .name = "Jane" });
+/// ```
+pub fn define(comptime T: type, comptime zon_data: anytype) type {
+ validateFixtureData(T, zon_data);
+
+ return struct {
+ /// Create an instance with optional field overrides
+ pub fn create(overrides: anytype) T {
+ return buildFixture(T, zon_data, overrides);
+ }
+ };
+}
+
+/// Build a fixture instance by merging .zon defaults with callsite overrides.
+fn buildFixture(comptime T: type, comptime zon_data: anytype, overrides: anytype) T {
+ var result: T = undefined;
+ const OverridesType = @TypeOf(overrides);
+
+ // Validate override fields exist in T (catch typos at compile time)
+ if (OverridesType != @TypeOf(.{})) {
+ inline for (std.meta.fields(OverridesType)) |override_field| {
+ if (!@hasField(T, override_field.name)) {
+ @compileError("Unknown override field '" ++ override_field.name ++ "'. " ++
+ "Type '" ++ @typeName(T) ++ "' has no such field.");
+ }
+ }
+ }
+
+ inline for (std.meta.fields(T)) |field| {
+ // Check for callsite override first
+ if (OverridesType != @TypeOf(.{}) and @hasField(OverridesType, field.name)) {
+ const override_value = @field(overrides, field.name);
+ const OverrideFieldType = @TypeOf(override_value);
+
+ // If both override and .zon default are structs, and the target is a struct,
+ // merge them field-by-field (partial nested override)
+ if (@typeInfo(field.type) == .@"struct" and
+ @typeInfo(OverrideFieldType) == .@"struct" and
+ @hasField(@TypeOf(zon_data), field.name))
+ {
+ @field(result, field.name) = mergeOverride(field.type, @field(zon_data, field.name), override_value);
+ } else {
+ @field(result, field.name) = resolveFieldValue(field.type, override_value);
+ }
+ }
+ // Use .zon default
+ else if (@hasField(@TypeOf(zon_data), field.name)) {
+ @field(result, field.name) = resolveFieldValue(field.type, @field(zon_data, field.name));
+ }
+ // Use the type's default value if available
+ else if (field.default_value_ptr) |default_ptr| {
+ const default_typed: *const field.type = @ptrCast(@alignCast(default_ptr));
+ @field(result, field.name) = default_typed.*;
+ } else {
+ @compileError("Fixture: no value for field '" ++ field.name ++ "' in type '" ++ @typeName(T) ++ "'. " ++
+ "Provide it in the .zon data or add a default value to the type.");
+ }
+ }
+
+ return result;
+}
+
+/// Merge an override struct with .zon defaults field-by-field.
+/// When both are structs, the override only replaces specified fields; unspecified
+/// fields fall back to .zon defaults. This enables partial nested overrides like
+/// `.create(.{ .user = .{ .name = "Jane" } })` preserving other user fields from .zon.
+fn mergeOverride(comptime FieldType: type, comptime zon_default: anytype, override: anytype) FieldType {
+ const OverrideType = @TypeOf(override);
+
+ var result: FieldType = undefined;
+ inline for (std.meta.fields(FieldType)) |field| {
+ if (@hasField(OverrideType, field.name)) {
+ // Override provides this field — use it (recursively merge if also struct)
+ const override_value = @field(override, field.name);
+ const OverrideFieldType = @TypeOf(override_value);
+
+ if (@typeInfo(field.type) == .@"struct" and
+ @typeInfo(OverrideFieldType) == .@"struct" and
+ @hasField(@TypeOf(zon_default), field.name))
+ {
+ @field(result, field.name) = mergeOverride(field.type, @field(zon_default, field.name), override_value);
+ } else {
+ @field(result, field.name) = resolveFieldValue(field.type, override_value);
+ }
+ } else if (@hasField(@TypeOf(zon_default), field.name)) {
+ // Fall back to .zon default
+ @field(result, field.name) = resolveFieldValue(field.type, @field(zon_default, field.name));
+ } else if (field.default_value_ptr) |default_ptr| {
+ const default_typed: *const field.type = @ptrCast(@alignCast(default_ptr));
+ @field(result, field.name) = default_typed.*;
+ } else {
+ @compileError("Fixture: no value for field '" ++ field.name ++ "'");
+ }
+ }
+ return result;
+}
+
+/// Resolve a single field value, handling type coercion for structs, unions, and arrays.
+fn resolveFieldValue(comptime FieldType: type, value: anytype) FieldType {
+ const ValueType = @TypeOf(value);
+
+ // Already the right type
+ if (ValueType == FieldType) {
+ return value;
+ }
+
+ // Fixed-size array: [N]T from a .zon tuple
+ if (@typeInfo(FieldType) == .array) {
+ return resolveArrayField(FieldType, value);
+ }
+
+ // Union from anonymous struct
+ if (@typeInfo(FieldType) == .@"union" and @typeInfo(ValueType) == .@"struct") {
+ return coerce.coerceToUnion(FieldType, value);
+ }
+
+ // Struct from anonymous struct (recursive coercion)
+ if (@typeInfo(FieldType) == .@"struct" and @typeInfo(ValueType) == .@"struct") {
+ return coerce.buildTypedPayload(FieldType, value);
+ }
+
+ // Direct coercion
+ return @as(FieldType, value);
+}
+
+/// Resolve a fixed-size array field from a .zon tuple.
+fn resolveArrayField(comptime ArrayType: type, value: anytype) ArrayType {
+ const array_info = @typeInfo(ArrayType).array;
+ const ElemType = array_info.child;
+ const ValueType = @TypeOf(value);
+
+ // If already the right type, return directly
+ if (ValueType == ArrayType) {
+ return value;
+ }
+
+ // Handle tuple (anonymous struct with numeric fields)
+ if (@typeInfo(ValueType) == .@"struct") {
+ if (!@typeInfo(ValueType).@"struct".is_tuple) {
+ @compileError("Fixture: array field expects a tuple (.{ val1, val2, ... }), got a named struct");
+ }
+ const value_fields = std.meta.fields(ValueType);
+ if (value_fields.len != array_info.len) {
+ @compileError(std.fmt.comptimePrint(
+ "Fixture: array field expects {d} elements but .zon tuple has {d}",
+ .{ array_info.len, value_fields.len },
+ ));
+ }
+
+ var result: ArrayType = undefined;
+ inline for (0..array_info.len) |i| {
+ result[i] = resolveFieldValue(ElemType, value[i]);
+ }
+ return result;
+ }
+
+ @compileError("Fixture: cannot coerce value to array type '" ++ @typeName(ArrayType) ++ "'");
+}
+
+/// Validate fixture data against the target type at compile time.
+/// Extends coerce.validateZonFields with array-aware validation.
+fn validateFixtureData(comptime T: type, comptime zon_data: anytype) void {
+ const ZonType = @TypeOf(zon_data);
+ const zon_fields = std.meta.fields(ZonType);
+
+ inline for (zon_fields) |zon_field| {
+ if (!@hasField(T, zon_field.name)) {
+ @compileError("Unknown field '" ++ zon_field.name ++ "' in fixture data. " ++
+ "Type '" ++ @typeName(T) ++ "' has no such field. " ++
+ "Check for typos in your .zon file.");
+ }
+
+ // Find the target field and recursively validate
+ inline for (std.meta.fields(T)) |target_field| {
+ if (comptime std.mem.eql(u8, target_field.name, zon_field.name)) {
+ const zon_field_value = @field(zon_data, zon_field.name);
+ const ZonFieldType = @TypeOf(zon_field_value);
+
+ // Array fields: validate each element
+ if (@typeInfo(target_field.type) == .array) {
+ const elem_type = @typeInfo(target_field.type).array.child;
+ if (@typeInfo(ZonFieldType) == .@"struct") {
+ // Validate each tuple element against the array element type
+ if (@typeInfo(elem_type) == .@"struct") {
+ inline for (0..std.meta.fields(ZonFieldType).len) |i| {
+ const elem = zon_field_value[i];
+ if (@typeInfo(@TypeOf(elem)) == .@"struct") {
+ validateFixtureData(elem_type, elem);
+ }
+ }
+ }
+ }
+ }
+ // Nested struct fields
+ else if (@typeInfo(target_field.type) == .@"struct" and @typeInfo(ZonFieldType) == .@"struct") {
+ coerce.validateZonFields(target_field.type, zon_field_value);
+ }
+ // Union fields
+ else if (@typeInfo(target_field.type) == .@"union" and @typeInfo(ZonFieldType) == .@"struct") {
+ coerce.validateUnionPayload(target_field.type, zon_field_value);
+ }
+ break;
+ }
+ }
+ }
+}
+
+// =============================================================================
+// Tests
+// =============================================================================
+
+test "basic fixture create" {
+ const User = struct {
+ name: []const u8,
+ age: u8,
+ active: bool,
+ };
+
+ const UserFixture = define(User, .{
+ .name = "John Doe",
+ .age = 25,
+ .active = true,
+ });
+
+ const user = UserFixture.create(.{});
+ try std.testing.expectEqualStrings("John Doe", user.name);
+ try std.testing.expectEqual(@as(u8, 25), user.age);
+ try std.testing.expect(user.active);
+}
+
+test "fixture create with overrides" {
+ const User = struct {
+ name: []const u8,
+ age: u8,
+ };
+
+ const UserFixture = define(User, .{
+ .name = "John",
+ .age = 25,
+ });
+
+ const user = UserFixture.create(.{ .name = "Jane", .age = 30 });
+ try std.testing.expectEqualStrings("Jane", user.name);
+ try std.testing.expectEqual(@as(u8, 30), user.age);
+}
+
+test "fixture with nested struct coercion" {
+ const Color = struct { r: u8, g: u8, b: u8 };
+ const Sprite = struct { tint: Color, scale: f32 };
+
+ const SpriteFixture = define(Sprite, .{
+ .tint = .{ .r = 255, .g = 128, .b = 64 },
+ .scale = 1.5,
+ });
+
+ const sprite = SpriteFixture.create(.{});
+ try std.testing.expectEqual(@as(u8, 255), sprite.tint.r);
+ try std.testing.expectEqual(@as(u8, 128), sprite.tint.g);
+ try std.testing.expectEqual(@as(u8, 64), sprite.tint.b);
+}
+
+test "fixture with array field" {
+ const Item = struct { id: u32, name: []const u8 };
+ const Inventory = struct {
+ owner: []const u8,
+ items: [2]Item,
+ };
+
+ const InvFixture = define(Inventory, .{
+ .owner = "Alice",
+ .items = .{
+ .{ .id = 1, .name = "Sword" },
+ .{ .id = 2, .name = "Shield" },
+ },
+ });
+
+ const inv = InvFixture.create(.{});
+ try std.testing.expectEqualStrings("Alice", inv.owner);
+ try std.testing.expectEqual(@as(u32, 1), inv.items[0].id);
+ try std.testing.expectEqualStrings("Sword", inv.items[0].name);
+ try std.testing.expectEqual(@as(u32, 2), inv.items[1].id);
+ try std.testing.expectEqualStrings("Shield", inv.items[1].name);
+}
+
+test "fixture scenario (struct of structs)" {
+ const User = struct { id: u32, name: []const u8 };
+ const Product = struct { id: u32, name: []const u8, seller_id: u32 };
+ const Scenario = struct { user: User, product: Product };
+
+ const CheckoutFixture = define(Scenario, .{
+ .user = .{ .id = 1, .name = "John" },
+ .product = .{ .id = 10, .name = "Widget", .seller_id = 1 },
+ });
+
+ const s = CheckoutFixture.create(.{});
+ try std.testing.expectEqual(@as(u32, 1), s.user.id);
+ try std.testing.expectEqualStrings("John", s.user.name);
+ try std.testing.expectEqual(@as(u32, 10), s.product.id);
+ try std.testing.expectEqual(s.product.seller_id, s.user.id);
+}
+
+test "partial nested override preserves .zon defaults" {
+ const User = struct { id: u32, name: []const u8, email: []const u8 };
+ const Scenario = struct { user: User };
+
+ const ScenarioFixture = define(Scenario, .{
+ .user = .{ .id = 1, .name = "John", .email = "john@example.com" },
+ });
+
+ // Override only name — id and email should come from .zon defaults
+ const s = ScenarioFixture.create(.{ .user = .{ .name = "Jane" } });
+ try std.testing.expectEqualStrings("Jane", s.user.name);
+ try std.testing.expectEqual(@as(u32, 1), s.user.id);
+ try std.testing.expectEqualStrings("john@example.com", s.user.email);
+}
+
+test "fixture with []const u8 fields" {
+ const Config = struct { host: []const u8, path: []const u8 };
+ const ConfigFixture = define(Config, .{ .host = "localhost", .path = "/api" });
+
+ const config = ConfigFixture.create(.{});
+ try std.testing.expectEqualStrings("localhost", config.host);
+ try std.testing.expectEqualStrings("/api", config.path);
+
+ // Override with different string
+ const custom = ConfigFixture.create(.{ .host = "example.com" });
+ try std.testing.expectEqualStrings("example.com", custom.host);
+ try std.testing.expectEqualStrings("/api", custom.path);
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/integrations/ecs.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/integrations/ecs.zig
new file mode 100644
index 0000000..12f052c
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/integrations/ecs.zig
@@ -0,0 +1,307 @@
+//! ZSpec ECS Integration - Helpers for using ZSpec with zig-ecs
+//!
+//! Provides utilities for creating entities and components using ZSpec factories
+//! in your zig-ecs tests. This module works with prime31/zig-ecs.
+//!
+//! Features:
+//! - createEntity() - Create entities with factory-generated components
+//! - createEntities() - Batch create multiple entities
+//! - ComponentFactory() - Wrapper for component-specific factories
+//! - Registry helpers for setup/teardown in before/after hooks
+//!
+//! Example usage:
+//! ```zig
+//! const zspec = @import("zspec");
+//! const ecs = @import("zig-ecs");
+//! const EcsHelpers = zspec.ECS;
+//!
+//! const PositionFactory = Factory.define(Position, .{
+//! .x = 0.0,
+//! .y = 0.0,
+//! });
+//!
+//! pub const EntityTests = struct {
+//! var registry: *ecs.Registry = undefined;
+//!
+//! test "tests:before" {
+//! registry = EcsHelpers.createRegistry(ecs.Registry);
+//! }
+//!
+//! test "tests:after" {
+//! EcsHelpers.destroyRegistry(registry);
+//! }
+//!
+//! test "creates entity with components" {
+//! const entity = EcsHelpers.createEntity(registry, .{
+//! .position = PositionFactory.build(.{ .x = 10.0 }),
+//! });
+//! // entity is created with Position component
+//! }
+//! };
+//! ```
+
+const std = @import("std");
+
+/// Create a registry instance for testing
+/// Usage in before hook:
+/// ```zig
+/// test "tests:before" {
+/// registry = ECS.createRegistry(ecs.Registry);
+/// }
+/// ```
+pub fn createRegistry(comptime RegistryType: type) *RegistryType {
+ const registry = std.testing.allocator.create(RegistryType) catch @panic("failed to create registry");
+ registry.* = RegistryType.init(std.testing.allocator);
+ return registry;
+}
+
+/// Create a registry instance with a custom allocator
+pub fn createRegistryWith(comptime RegistryType: type, allocator: std.mem.Allocator) *RegistryType {
+ const registry = allocator.create(RegistryType) catch @panic("failed to create registry");
+ registry.* = RegistryType.init(allocator);
+ return registry;
+}
+
+/// Destroy a registry instance (for use in after hook)
+/// Usage in after hook:
+/// ```zig
+/// test "tests:after" {
+/// ECS.destroyRegistry(registry);
+/// }
+/// ```
+pub fn destroyRegistry(registry: anytype) void {
+ registry.deinit();
+
+ // Get the allocator that was used to create the registry
+ // We assume it's std.testing.allocator by default
+ std.testing.allocator.destroy(registry);
+}
+
+/// Destroy a registry instance with a custom allocator
+pub fn destroyRegistryWith(registry: anytype, allocator: std.mem.Allocator) void {
+ registry.deinit();
+ allocator.destroy(registry);
+}
+
+/// Component data for entity creation
+/// Pass an anonymous struct with component field names and their data
+pub fn ComponentSet(comptime T: type) type {
+ return T;
+}
+
+/// Create a single entity with components from an anonymous struct
+///
+/// Example:
+/// ```zig
+/// const entity = ECS.createEntity(registry, .{
+/// .position = PositionFactory.build(.{}),
+/// .velocity = VelocityFactory.build(.{ .dx = 5.0 }),
+/// });
+/// ```
+pub fn createEntity(registry: anytype, components: anytype) EntityType(@TypeOf(registry)) {
+ const entity = registry.create();
+
+ inline for (std.meta.fields(@TypeOf(components))) |field| {
+ const component = @field(components, field.name);
+ registry.add(entity, component);
+ }
+
+ return entity;
+}
+
+/// Helper to extract Entity type from a registry pointer type
+fn EntityType(comptime RegistryPtrType: type) type {
+ // Get the underlying struct type from the pointer
+ const RegistryType = std.meta.Child(RegistryPtrType);
+ // Get the create function and extract its return type
+ const create_fn = @field(RegistryType, "create");
+ const create_fn_info = @typeInfo(@TypeOf(create_fn));
+ return create_fn_info.@"fn".return_type.?;
+}
+
+/// Create multiple entities with the same component configuration
+/// Returns a slice of entity IDs (allocated with std.testing.allocator)
+///
+/// Example:
+/// ```zig
+/// const entities = ECS.createEntities(registry, 5, .{
+/// .position = PositionFactory.build(.{}),
+/// });
+/// defer std.testing.allocator.free(entities);
+/// ```
+pub fn createEntities(registry: anytype, count: usize, components: anytype) []EntityType(@TypeOf(registry)) {
+ return createEntitiesWith(registry, count, components, std.testing.allocator);
+}
+
+/// Create multiple entities with custom allocator
+pub fn createEntitiesWith(registry: anytype, count: usize, components: anytype, allocator: std.mem.Allocator) []EntityType(@TypeOf(registry)) {
+ const Entity = EntityType(@TypeOf(registry));
+ const entities = allocator.alloc(Entity, count) catch @panic("failed to allocate entities array");
+
+ for (entities) |*entity| {
+ entity.* = createEntity(registry, components);
+ }
+
+ return entities;
+}
+
+/// Create multiple entities with unique components using a callback
+/// This allows each entity to have unique component values (e.g., sequences)
+///
+/// Example:
+/// ```zig
+/// const EntityBuilder = struct {
+/// pub fn build() @TypeOf(.{
+/// .position = PositionFactory.build(.{}),
+/// }) {
+/// return .{
+/// .position = PositionFactory.build(.{}),
+/// .id = IdFactory.build(.{}), // uses sequence for unique IDs
+/// };
+/// }
+/// };
+///
+/// const entities = ECS.createEntitiesUnique(registry, 5, EntityBuilder);
+/// defer std.testing.allocator.free(entities);
+/// ```
+pub fn createEntitiesUnique(
+ registry: anytype,
+ count: usize,
+ comptime Builder: type,
+) []EntityType(@TypeOf(registry)) {
+ return createEntitiesUniqueWith(registry, count, Builder, std.testing.allocator);
+}
+
+/// Create multiple entities with unique components using a builder type and custom allocator
+pub fn createEntitiesUniqueWith(
+ registry: anytype,
+ count: usize,
+ comptime Builder: type,
+ allocator: std.mem.Allocator,
+) []EntityType(@TypeOf(registry)) {
+ const Entity = EntityType(@TypeOf(registry));
+ const entities = allocator.alloc(Entity, count) catch @panic("failed to allocate entities array");
+
+ for (entities) |*entity| {
+ const components = Builder.build();
+ entity.* = createEntity(registry, components);
+ }
+
+ return entities;
+}
+
+/// Helper to create a Let-style memoized registry
+///
+/// Example:
+/// ```zig
+/// pub const MyTests = struct {
+/// fn initRegistry() *ecs.Registry {
+/// return ECS.createRegistry(ecs.Registry);
+/// }
+///
+/// const registry = zspec.LetAlloc(*ecs.Registry, initRegistry);
+///
+/// test "tests:after" {
+/// ECS.destroyRegistry(registry.get());
+/// registry.reset();
+/// }
+/// };
+/// ```
+pub fn RegistryLet(comptime RegistryType: type) type {
+ return struct {
+ pub fn init() *RegistryType {
+ return createRegistry(RegistryType);
+ }
+ };
+}
+
+// =============================================================================
+// Component Factory Pattern
+// =============================================================================
+
+/// Wrapper for a factory that produces component data
+/// Useful for creating reusable component builders
+///
+/// Example:
+/// ```zig
+/// const PositionComponent = ECS.ComponentFactory(Position, PositionFactory);
+///
+/// const entity = registry.create();
+/// PositionComponent.attach(registry, entity, .{ .x = 10.0 });
+/// ```
+pub fn ComponentFactory(comptime ComponentType: type, comptime Factory: type) type {
+ return struct {
+ /// Build component data with overrides
+ pub fn build(overrides: anytype) ComponentType {
+ return Factory.build(overrides);
+ }
+
+ /// Build component data with custom allocator
+ pub fn buildWith(allocator: std.mem.Allocator, overrides: anytype) ComponentType {
+ return Factory.buildWith(allocator, overrides);
+ }
+
+ /// Create and attach component to an existing entity
+ pub fn attach(registry: anytype, entity: anytype, overrides: anytype) void {
+ const component = build(overrides);
+ registry.add(entity, component);
+ }
+
+ /// Create and attach component with custom allocator
+ pub fn attachWith(registry: anytype, entity: anytype, allocator: std.mem.Allocator, overrides: anytype) void {
+ const component = buildWith(allocator, overrides);
+ registry.add(entity, component);
+ }
+
+ /// Create a new entity with this component
+ pub fn createEntityWith(registry: anytype, overrides: anytype) EntityType(@TypeOf(registry)) {
+ const entity = registry.create();
+ attach(registry, entity, overrides);
+ return entity;
+ }
+
+ /// Create multiple entities with this component
+ pub fn createEntitiesWith(registry: anytype, count: usize, overrides: anytype) []EntityType(@TypeOf(registry)) {
+ const Entity = EntityType(@TypeOf(registry));
+ const entities = std.testing.allocator.alloc(Entity, count) catch @panic("failed to allocate entities");
+
+ for (entities) |*e| {
+ e.* = createEntityWith(registry, overrides);
+ }
+
+ return entities;
+ }
+ };
+}
+
+// =============================================================================
+// Testing Patterns
+// =============================================================================
+
+/// Example pattern for setting up ECS tests with Let
+///
+/// ```zig
+/// pub const MyECSTests = struct {
+/// // Use Let for lazy registry creation
+/// fn createTestRegistry() *ecs.Registry {
+/// return ECS.createRegistry(ecs.Registry);
+/// }
+///
+/// const registry = zspec.LetAlloc(*ecs.Registry, createTestRegistry);
+///
+/// test "tests:after" {
+/// ECS.destroyRegistry(registry.get());
+/// registry.reset();
+/// }
+///
+/// test "my test" {
+/// const entity = ECS.createEntity(registry.get(), .{
+/// .position = PositionFactory.build(.{}),
+/// });
+/// // test code...
+/// }
+/// };
+/// ```
+pub const TestPattern = struct {
+ // This is just documentation - see examples/ecs_integration_test.zig
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/integrations/fsm.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/integrations/fsm.zig
new file mode 100644
index 0000000..d94274e
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/integrations/fsm.zig
@@ -0,0 +1,204 @@
+//! ZSpec FSM Integration - Helpers for using ZSpec with zigfsm
+//!
+//! Provides utilities for testing finite state machines using ZSpec factories.
+//! This module works with cryptocode/zigfsm (https://github.com/cryptocode/zigfsm).
+//!
+//! Features:
+//! - FSM factory patterns for common state machine configurations
+//! - Transition builders for readable test setup
+//! - State verification helpers
+//! - Event testing utilities
+//!
+//! Example usage:
+//! ```zig
+//! const zspec = @import("zspec");
+//! const zigfsm = @import("zigfsm");
+//! const FSMHelpers = zspec.FSM;
+//!
+//! const State = enum { idle, running, stopped };
+//! const Event = enum { start, stop };
+//!
+//! pub const FSMTests = struct {
+//! const FSM = zigfsm.StateMachine(State, Event, .idle);
+//! var fsm: FSM = undefined;
+//!
+//! test "tests:before" {
+//! fsm = FSM.init();
+//! try FSMHelpers.addTransitions(FSM, &fsm, &.{
+//! .{ .event = .start, .from = .idle, .to = .running },
+//! .{ .event = .stop, .from = .running, .to = .stopped },
+//! });
+//! }
+//!
+//! test "state transitions work" {
+//! try fsm.do(.start);
+//! try expect.toBeTrue(fsm.isCurrently(.running));
+//! }
+//! };
+//! ```
+
+const std = @import("std");
+
+/// Transition definition for test setup
+pub fn Transition(comptime State: type, comptime Event: type) type {
+ return struct {
+ event: Event,
+ from: State,
+ to: State,
+ };
+}
+
+/// Add multiple transitions to a state machine for test setup
+/// This is a helper to make test setup more concise
+pub fn addTransitions(
+ comptime FSMType: type,
+ fsm: *FSMType,
+ transitions: []const Transition(@TypeOf(fsm.*.state), FSMType.Event),
+) !void {
+ for (transitions) |t| {
+ try fsm.addEventAndTransition(t.event, t.from, t.to);
+ }
+}
+
+/// Builder pattern for setting up state machines in tests
+/// Allows fluent configuration of FSMs
+pub fn FSMBuilder(comptime FSMType: type) type {
+ return struct {
+ fsm: FSMType,
+
+ const Self = @This();
+
+ pub fn init() Self {
+ return .{ .fsm = FSMType.init() };
+ }
+
+ /// Add a transition between states
+ pub fn withTransition(self: *Self, from: @TypeOf(self.fsm.state), to: @TypeOf(self.fsm.state)) !*Self {
+ try self.fsm.addTransition(from, to);
+ return self;
+ }
+
+ /// Add an event-based transition
+ pub fn withEvent(
+ self: *Self,
+ event: FSMType.Event,
+ from: @TypeOf(self.fsm.state),
+ to: @TypeOf(self.fsm.state),
+ ) !*Self {
+ try self.fsm.addEventAndTransition(event, from, to);
+ return self;
+ }
+
+ /// Build and return the configured FSM
+ pub fn build(self: Self) FSMType {
+ return self.fsm;
+ }
+
+ /// Get a mutable reference to the FSM
+ pub fn get(self: *Self) *FSMType {
+ return &self.fsm;
+ }
+ };
+}
+
+/// Verify a sequence of state transitions
+/// Useful for testing complex state flows
+pub fn verifySequence(
+ comptime FSMType: type,
+ fsm: *FSMType,
+ expected_states: []const @TypeOf(fsm.*.state),
+) !void {
+ for (expected_states) |expected_state| {
+ if (!fsm.isCurrently(expected_state)) {
+ std.debug.print(
+ "\nExpected state: {any}\nActual state: {any}\n",
+ .{ expected_state, fsm.state },
+ );
+ return error.StateSequenceMismatch;
+ }
+ }
+}
+
+/// Apply a sequence of events and verify the FSM ends in the expected state
+pub fn applyEventsAndVerify(
+ comptime FSMType: type,
+ fsm: *FSMType,
+ events: []const FSMType.Event,
+ expected_final_state: @TypeOf(fsm.*.state),
+) !void {
+ for (events) |event| {
+ try fsm.do(event);
+ }
+
+ if (!fsm.isCurrently(expected_final_state)) {
+ std.debug.print(
+ "\nExpected final state: {any}\nActual state: {any}\n",
+ .{ expected_final_state, fsm.state },
+ );
+ return error.FinalStateMismatch;
+ }
+}
+
+/// Test helper to verify valid next states
+pub fn expectValidNextStates(
+ comptime FSMType: type,
+ fsm: *FSMType,
+ expected_states: []const @TypeOf(fsm.*.state),
+) !void {
+ for (expected_states) |state| {
+ if (!fsm.canTransitionTo(state)) {
+ std.debug.print(
+ "\nExpected valid transition to: {any}\nBut transition is not valid\n",
+ .{state},
+ );
+ return error.InvalidTransition;
+ }
+ }
+}
+
+/// Test helper to verify invalid next states
+pub fn expectInvalidNextStates(
+ comptime FSMType: type,
+ fsm: *FSMType,
+ invalid_states: []const @TypeOf(fsm.*.state),
+) !void {
+ for (invalid_states) |state| {
+ if (fsm.canTransitionTo(state)) {
+ std.debug.print(
+ "\nExpected invalid transition to: {any}\nBut transition is valid\n",
+ .{state},
+ );
+ return error.UnexpectedValidTransition;
+ }
+ }
+}
+
+// =============================================================================
+// Testing Patterns
+// =============================================================================
+
+/// Example pattern for setting up FSM tests with before/after hooks
+///
+/// ```zig
+/// pub const MyFSMTests = struct {
+/// const State = enum { idle, active, done };
+/// const Event = enum { start, finish };
+/// const FSM = zigfsm.StateMachine(State, Event, .idle);
+///
+/// var fsm: FSM = undefined;
+///
+/// test "tests:before" {
+/// fsm = FSM.init();
+/// try fsm.addEventAndTransition(.start, .idle, .active);
+/// try fsm.addEventAndTransition(.finish, .active, .done);
+/// }
+///
+/// test "state transitions" {
+/// try fsm.do(.start);
+/// try expect.toBeTrue(fsm.isCurrently(.active));
+/// }
+/// };
+/// ```
+pub const TestPattern = struct {
+ // This is just documentation - see examples/fsm_integration_test.zig
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/junit.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/junit.zig
new file mode 100644
index 0000000..ef6f3d6
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/junit.zig
@@ -0,0 +1,449 @@
+//! JUnit XML Report Writer
+//!
+//! Generates JUnit XML format test reports compatible with CI systems
+//! like Jenkins, GitHub Actions, GitLab CI, etc.
+//!
+//! JUnit XML Schema Reference:
+//! https://github.com/testmoapp/junitxml
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+pub const TestResult = struct {
+ name: []const u8,
+ classname: []const u8,
+ time_ns: u64,
+ status: Status,
+ failure_message: ?[]const u8 = null,
+ failure_type: ?[]const u8 = null,
+
+ pub const Status = enum {
+ passed,
+ failed,
+ skipped,
+ };
+};
+
+pub const TestSuite = struct {
+ name: []const u8,
+ tests: usize,
+ failures: usize,
+ skipped: usize,
+ time_ns: u64,
+ timestamp: []const u8,
+};
+
+pub const JUnitWriter = struct {
+ allocator: Allocator,
+ results: std.ArrayListUnmanaged(TestResult),
+ suite_name: []const u8,
+ start_time: i64,
+
+ pub fn init(allocator: Allocator, suite_name: []const u8) JUnitWriter {
+ return .{
+ .allocator = allocator,
+ .results = .empty,
+ .suite_name = suite_name,
+ .start_time = timestampSeconds(),
+ };
+ }
+
+ fn timestampSeconds() i64 {
+ // `std.time.timestamp` was removed in 0.16; query the real-time clock
+ // directly to keep this module independent of an `Io` instance.
+ const native_os = @import("builtin").os.tag;
+ switch (native_os) {
+ .windows => {
+ // Windows: FILETIME -> Unix seconds. The std.os.windows.kernel32
+ // wrappers were trimmed in 0.16, so declare the import directly.
+ const w = std.os.windows;
+ const k32 = struct {
+ extern "kernel32" fn GetSystemTimeAsFileTime(lpSystemTimeAsFileTime: *w.FILETIME) callconv(.winapi) void;
+ };
+ var ft: w.FILETIME = undefined;
+ k32.GetSystemTimeAsFileTime(&ft);
+ const ticks: i64 = (@as(i64, ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
+ const unix_epoch_offset: i64 = 11644473600;
+ return @divTrunc(ticks, 10_000_000) - unix_epoch_offset;
+ },
+ else => {
+ var ts: std.posix.timespec = undefined;
+ _ = std.posix.system.clock_gettime(.REALTIME, &ts);
+ return ts.sec;
+ },
+ }
+ }
+
+ pub fn deinit(self: *JUnitWriter) void {
+ self.results.deinit(self.allocator);
+ }
+
+ pub fn addResult(self: *JUnitWriter, result: TestResult) !void {
+ try self.results.append(self.allocator, result);
+ }
+
+ pub fn writeToFile(self: *JUnitWriter, path: []const u8) !void {
+ // Build the XML fully in memory, then dump it through `writeAllToPath`,
+ // which uses Win32 `CreateFileW`/`WriteFile` on Windows and libc
+ // `open`/`write` elsewhere. Doing the entire I/O dance via `std.Io`
+ // would require plumbing an `Io` instance through the test runner,
+ // which is overkill here.
+ var aw: std.Io.Writer.Allocating = .init(self.allocator);
+ defer aw.deinit();
+
+ try self.write(&aw.writer);
+
+ const xml = aw.writer.buffered();
+ try writeAllToPath(path, xml);
+ }
+
+ fn writeAllToPath(path: []const u8, bytes: []const u8) !void {
+ // Reject paths with an interior NUL byte before we convert to a
+ // C/WTF-16 string. Without this guard `"report.xml\x00ignored"` would
+ // be silently truncated to `"report.xml"` by both libc `open` (NUL
+ // terminator) and `CreateFileW` (WTF-16 NUL terminator).
+ if (std.mem.indexOfScalar(u8, path, 0) != null) return error.InvalidPath;
+
+ const native_os = @import("builtin").os.tag;
+ switch (native_os) {
+ .windows => {
+ // Use the Win32 file API directly. `std.c.O` is `void` on
+ // Windows in 0.16, so we can't reuse the POSIX path here.
+ const w = std.os.windows;
+ const k32 = struct {
+ extern "kernel32" fn CreateFileW(
+ lpFileName: w.LPCWSTR,
+ dwDesiredAccess: w.DWORD,
+ dwShareMode: w.DWORD,
+ lpSecurityAttributes: ?*anyopaque,
+ dwCreationDisposition: w.DWORD,
+ dwFlagsAndAttributes: w.DWORD,
+ hTemplateFile: ?w.HANDLE,
+ ) callconv(.winapi) w.HANDLE;
+ extern "kernel32" fn WriteFile(
+ hFile: w.HANDLE,
+ lpBuffer: [*]const u8,
+ nNumberOfBytesToWrite: w.DWORD,
+ lpNumberOfBytesWritten: *w.DWORD,
+ lpOverlapped: ?*anyopaque,
+ ) callconv(.winapi) w.BOOL;
+ };
+ const GENERIC_WRITE: w.DWORD = 0x40000000;
+ const CREATE_ALWAYS: w.DWORD = 2;
+ const FILE_ATTRIBUTE_NORMAL: w.DWORD = 0x80;
+
+ // Convert path to WTF-16 (null-terminated).
+ var path_buf_w: [std.fs.max_path_bytes]u16 = undefined;
+ const path_len_w = std.unicode.wtf8ToWtf16Le(&path_buf_w, path) catch return error.InvalidPath;
+ if (path_len_w >= path_buf_w.len) return error.NameTooLong;
+ path_buf_w[path_len_w] = 0;
+ const path_z: w.LPCWSTR = @ptrCast(&path_buf_w);
+
+ const h = k32.CreateFileW(path_z, GENERIC_WRITE, 0, null, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, null);
+ if (h == w.INVALID_HANDLE_VALUE) return error.FileOpenFailed;
+ defer w.CloseHandle(h);
+
+ var remaining = bytes;
+ while (remaining.len > 0) {
+ const chunk_len: w.DWORD = @intCast(@min(remaining.len, std.math.maxInt(w.DWORD)));
+ var written: w.DWORD = 0;
+ const ok = k32.WriteFile(h, remaining.ptr, chunk_len, &written, null);
+ if (!ok.toBool() or written == 0) return error.WriteFailed;
+ remaining = remaining[written..];
+ }
+ },
+ else => {
+ var path_buf: [std.fs.max_path_bytes:0]u8 = undefined;
+ if (path.len >= path_buf.len) return error.NameTooLong;
+ @memcpy(path_buf[0..path.len], path);
+ path_buf[path.len] = 0;
+ const c = std.c;
+ const flags: c.O = .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true };
+ const fd_int = c.open(@ptrCast(&path_buf), flags, @as(c.mode_t, 0o644));
+ if (fd_int < 0) return error.FileOpenFailed;
+ defer _ = c.close(fd_int);
+ var remaining = bytes;
+ while (remaining.len > 0) {
+ const w_ret = c.write(fd_int, remaining.ptr, remaining.len);
+ if (w_ret < 0) return error.WriteFailed;
+ remaining = remaining[@intCast(w_ret)..];
+ }
+ },
+ }
+ }
+
+ pub fn write(self: *JUnitWriter, writer: anytype) !void {
+ var total_time_ns: u64 = 0;
+ var failures: usize = 0;
+ var skipped: usize = 0;
+
+ for (self.results.items) |result| {
+ total_time_ns += result.time_ns;
+ switch (result.status) {
+ .failed => failures += 1,
+ .skipped => skipped += 1,
+ .passed => {},
+ }
+ }
+
+ const total_time_s = @as(f64, @floatFromInt(total_time_ns)) / 1_000_000_000.0;
+
+ // XML declaration
+ try writer.writeAll("\n");
+
+ // Testsuites root element
+ try writer.print(
+ "\n",
+ .{ self.results.items.len, failures, skipped, total_time_s },
+ );
+
+ // Testsuite element
+ try writer.print(
+ " \n",
+ .{ self.suite_name, self.results.items.len, failures, skipped, total_time_s, self.start_time },
+ );
+
+ // Test cases
+ for (self.results.items) |result| {
+ const time_s = @as(f64, @floatFromInt(result.time_ns)) / 1_000_000_000.0;
+
+ try writer.print(
+ " {
+ try writer.writeAll("/>\n");
+ },
+ .failed => {
+ try writer.writeAll(">\n");
+ try writer.writeAll(" \n");
+ try writer.writeAll(" \n");
+ },
+ .skipped => {
+ try writer.writeAll(">\n");
+ try writer.writeAll(" \n");
+ try writer.writeAll(" \n");
+ },
+ }
+ }
+
+ try writer.writeAll(" \n");
+ try writer.writeAll("\n");
+ }
+};
+
+fn writeEscaped(writer: anytype, str: []const u8) !void {
+ for (str) |c| {
+ switch (c) {
+ '<' => try writer.writeAll("<"),
+ '>' => try writer.writeAll(">"),
+ '&' => try writer.writeAll("&"),
+ '"' => try writer.writeAll("""),
+ '\'' => try writer.writeAll("'"),
+ else => {
+ const bytes = [_]u8{c};
+ try writer.writeAll(&bytes);
+ },
+ }
+ }
+}
+
+// Extract classname from test name (e.g., "module.submodule.test.test name" -> "module.submodule")
+pub fn extractClassname(test_name: []const u8) []const u8 {
+ // Find the last ".test." or ".test_" to get the module path
+ if (std.mem.lastIndexOf(u8, test_name, ".test.")) |idx| {
+ return test_name[0..idx];
+ }
+ if (std.mem.lastIndexOf(u8, test_name, ".test_")) |idx| {
+ return test_name[0..idx];
+ }
+ return test_name;
+}
+
+// Extract friendly test name (part after ".test.")
+pub fn extractTestName(test_name: []const u8) []const u8 {
+ var it = std.mem.splitScalar(u8, test_name, '.');
+ while (it.next()) |value| {
+ if (std.mem.eql(u8, value, "test")) {
+ const rest = it.rest();
+ return if (rest.len > 0) rest else test_name;
+ }
+ }
+ return test_name;
+}
+
+test "extractClassname" {
+ const expect = std.testing.expect;
+
+ const result1 = extractClassname("example_test.Calculator.test.adds numbers");
+ try expect(std.mem.eql(u8, result1, "example_test.Calculator"));
+
+ const result2 = extractClassname("module.submodule.TestStruct.test.my test");
+ try expect(std.mem.eql(u8, result2, "module.submodule.TestStruct"));
+
+ const result3 = extractClassname("simple_test");
+ try expect(std.mem.eql(u8, result3, "simple_test"));
+}
+
+test "extractTestName" {
+ const expect = std.testing.expect;
+
+ const result1 = extractTestName("example_test.Calculator.test.adds numbers");
+ try expect(std.mem.eql(u8, result1, "adds numbers"));
+
+ const result2 = extractTestName("simple_test");
+ try expect(std.mem.eql(u8, result2, "simple_test"));
+}
+
+test "JUnitWriter generates valid XML" {
+ const allocator = std.testing.allocator;
+
+ var writer = JUnitWriter.init(allocator, "test-suite");
+ defer writer.deinit();
+
+ try writer.addResult(.{
+ .name = "test one",
+ .classname = "MyClass",
+ .time_ns = 1_000_000,
+ .status = .passed,
+ });
+
+ try writer.addResult(.{
+ .name = "test two",
+ .classname = "MyClass",
+ .time_ns = 2_000_000,
+ .status = .failed,
+ .failure_message = "expected 1, got 2",
+ .failure_type = "AssertionError",
+ });
+
+ try writer.addResult(.{
+ .name = "test three",
+ .classname = "MyClass",
+ .time_ns = 500_000,
+ .status = .skipped,
+ });
+
+ var aw: std.Io.Writer.Allocating = .init(allocator);
+ defer aw.deinit();
+
+ try writer.write(&aw.writer);
+
+ const xml = aw.writer.buffered();
+
+ // Verify XML structure
+ try std.testing.expect(std.mem.indexOf(u8, xml, "") != null);
+}
+
+test "writeToFile writes XML to disk" {
+ const allocator = std.testing.allocator;
+ const io = std.testing.io;
+
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+
+ // `writeToFile` opens via libc `open` / Win32 `CreateFileW`, both of which
+ // resolve relative to the process cwd. `tmpDir` creates
+ // `.zig-cache/tmp//`, so build a cwd-relative path to drop the
+ // report into and then read it back through `Io.Dir.readFileAlloc`.
+ const file_name = "report.xml";
+ const cwd_path = try std.fs.path.join(allocator, &.{ ".zig-cache", "tmp", &tmp.sub_path, file_name });
+ defer allocator.free(cwd_path);
+
+ var writer = JUnitWriter.init(allocator, "file-suite");
+ defer writer.deinit();
+
+ try writer.addResult(.{
+ .name = "writes to disk",
+ .classname = "FileTest",
+ .time_ns = 1_500_000,
+ .status = .passed,
+ });
+
+ try writer.writeToFile(cwd_path);
+
+ // Read it back through the tmp `Io.Dir` and verify the on-disk contents
+ // contain the expected XML structure.
+ const contents = try tmp.dir.readFileAlloc(io, file_name, allocator, .limited(1 << 20));
+ defer allocator.free(contents);
+
+ try std.testing.expect(std.mem.indexOf(u8, contents, " \"special\" & 'chars'",
+ .classname = "Test",
+ .time_ns = 1_000_000,
+ .status = .passed,
+ });
+
+ var aw: std.Io.Writer.Allocating = .init(allocator);
+ defer aw.deinit();
+
+ try writer.write(&aw.writer);
+
+ const xml = aw.writer.buffered();
+
+ // Verify escaping
+ try std.testing.expect(std.mem.indexOf(u8, xml, "<with>") != null);
+ try std.testing.expect(std.mem.indexOf(u8, xml, ""special"") != null);
+ try std.testing.expect(std.mem.indexOf(u8, xml, "&") != null);
+ try std.testing.expect(std.mem.indexOf(u8, xml, "'chars'") != null);
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/matchers.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/matchers.zig
new file mode 100644
index 0000000..0393ad4
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/matchers.zig
@@ -0,0 +1,402 @@
+//! ZSpec Fluent Matchers
+//!
+//! Provides RSpec/Jest-style fluent assertions:
+//! try expect(actual).to().equal(expected);
+//! try expect(condition).to().beTrue();
+//! try expect(value).notTo().beNull();
+//! try expect(slice).to().contain("needle");
+//!
+//! Supports negation via .notTo():
+//! try expect(x).notTo().equal(y);
+//! try expect(opt).notTo().beNull();
+//!
+//! Available Matchers:
+//! - Equality: equal(), eql() (deep equality)
+//! - Boolean: beTrue(), beFalse()
+//! - Null: beNull()
+//! - Comparison: beGreaterThan(), beLessThan(), beGreaterThanOrEqual(),
+//! beLessThanOrEqual(), beBetween()
+//! - String/Slice: contain(), startWith(), endWith(), haveLength(), beEmpty()
+//! - Type: beOfType()
+
+const std = @import("std");
+
+/// Creates a fluent matcher for the given value.
+/// Usage: try expect(value).to().equal(expected);
+pub fn expect(value: anytype) Matcher(@TypeOf(value)) {
+ return Matcher(@TypeOf(value)).init(value);
+}
+
+/// Fluent matcher type that provides chainable assertions.
+pub fn Matcher(comptime T: type) type {
+ return struct {
+ value: T,
+
+ const Self = @This();
+
+ pub fn init(value: T) Self {
+ return .{ .value = value };
+ }
+
+ /// Bridge to matchers namespace (positive assertion).
+ /// Usage: expect(x).to().equal(y)
+ pub fn to(self: Self) ToMatcher(T, false) {
+ return ToMatcher(T, false).init(self.value);
+ }
+
+ /// Bridge to matchers namespace (negated assertion).
+ /// Usage: expect(x).notTo().equal(y)
+ pub fn notTo(self: Self) ToMatcher(T, true) {
+ return ToMatcher(T, true).init(self.value);
+ }
+ };
+}
+
+/// The actual matcher implementations.
+fn ToMatcher(comptime T: type, comptime negated: bool) type {
+ return struct {
+ value: T,
+
+ const Self = @This();
+
+ pub fn init(value: T) Self {
+ return .{ .value = value };
+ }
+
+ // =========================================================
+ // Equality Matchers
+ // =========================================================
+
+ /// Asserts that actual equals expected (pointer equality for slices).
+ pub fn equal(self: Self, expected: T) !void {
+ const matches = self.value == expected;
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected {any} to NOT equal {any}\n", .{ self.value, expected });
+ } else {
+ std.debug.print("\n Expected: {any}\n Actual: {any}\n", .{ expected, self.value });
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ /// Asserts deep equality for slices/arrays/structs.
+ pub fn eql(self: Self, expected: T) !void {
+ const matches = std.meta.eql(self.value, expected);
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected values to NOT be deeply equal\n", .{});
+ } else {
+ std.debug.print("\n Expected deep equality\n Expected: {any}\n Actual: {any}\n", .{ expected, self.value });
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ // =========================================================
+ // Boolean Matchers
+ // =========================================================
+
+ /// Asserts that value is true.
+ pub fn beTrue(self: Self) !void {
+ const is_bool = T == bool;
+ if (!is_bool) {
+ @compileError("beTrue() requires a bool value");
+ }
+ const matches = self.value == true;
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected false, got true\n", .{});
+ } else {
+ std.debug.print("\n Expected true, got false\n", .{});
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ /// Asserts that value is false.
+ pub fn beFalse(self: Self) !void {
+ const is_bool = T == bool;
+ if (!is_bool) {
+ @compileError("beFalse() requires a bool value");
+ }
+ const matches = self.value == false;
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected true, got false\n", .{});
+ } else {
+ std.debug.print("\n Expected false, got true\n", .{});
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ // =========================================================
+ // Null/Optional Matchers
+ // =========================================================
+
+ /// Asserts that value is null.
+ pub fn beNull(self: Self) !void {
+ const matches = self.value == null;
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected non-null value, got null\n", .{});
+ } else {
+ std.debug.print("\n Expected null, got {any}\n", .{self.value});
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ // =========================================================
+ // Comparison Matchers
+ // =========================================================
+
+ /// Asserts that actual > expected.
+ pub fn beGreaterThan(self: Self, expected: T) !void {
+ const matches = self.value > expected;
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected {any} to NOT be greater than {any}\n", .{ self.value, expected });
+ } else {
+ std.debug.print("\n Expected {any} > {any}\n", .{ self.value, expected });
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ /// Asserts that actual >= expected.
+ pub fn beGreaterThanOrEqual(self: Self, expected: T) !void {
+ const matches = self.value >= expected;
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected {any} to NOT be >= {any}\n", .{ self.value, expected });
+ } else {
+ std.debug.print("\n Expected {any} >= {any}\n", .{ self.value, expected });
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ /// Asserts that actual < expected.
+ pub fn beLessThan(self: Self, expected: T) !void {
+ const matches = self.value < expected;
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected {any} to NOT be less than {any}\n", .{ self.value, expected });
+ } else {
+ std.debug.print("\n Expected {any} < {any}\n", .{ self.value, expected });
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ /// Asserts that actual <= expected.
+ pub fn beLessThanOrEqual(self: Self, expected: T) !void {
+ const matches = self.value <= expected;
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected {any} to NOT be <= {any}\n", .{ self.value, expected });
+ } else {
+ std.debug.print("\n Expected {any} <= {any}\n", .{ self.value, expected });
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ /// Asserts that actual is between min and max (inclusive).
+ pub fn beBetween(self: Self, min: T, max: T) !void {
+ const matches = self.value >= min and self.value <= max;
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected {any} to NOT be between {any} and {any}\n", .{ self.value, min, max });
+ } else {
+ std.debug.print("\n Expected {any} to be between {any} and {any}\n", .{ self.value, min, max });
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ // =========================================================
+ // String/Slice Matchers
+ // =========================================================
+
+ /// Asserts that haystack contains needle.
+ pub fn contain(self: Self, needle: []const u8) !void {
+ const haystack = self.value;
+ const matches = std.mem.indexOf(u8, haystack, needle) != null;
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected \"{s}\" to NOT contain \"{s}\"\n", .{ haystack, needle });
+ } else {
+ std.debug.print("\n Expected \"{s}\" to contain \"{s}\"\n", .{ haystack, needle });
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ /// Asserts that string starts with prefix.
+ pub fn startWith(self: Self, prefix: []const u8) !void {
+ const haystack = self.value;
+ const matches = std.mem.startsWith(u8, haystack, prefix);
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected \"{s}\" to NOT start with \"{s}\"\n", .{ haystack, prefix });
+ } else {
+ std.debug.print("\n Expected \"{s}\" to start with \"{s}\"\n", .{ haystack, prefix });
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ /// Asserts that string ends with suffix.
+ pub fn endWith(self: Self, suffix: []const u8) !void {
+ const haystack = self.value;
+ const matches = std.mem.endsWith(u8, haystack, suffix);
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected \"{s}\" to NOT end with \"{s}\"\n", .{ haystack, suffix });
+ } else {
+ std.debug.print("\n Expected \"{s}\" to end with \"{s}\"\n", .{ haystack, suffix });
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ /// Asserts that slice has expected length.
+ pub fn haveLength(self: Self, expected_len: usize) !void {
+ const actual_len = self.value.len;
+ const matches = actual_len == expected_len;
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected length to NOT be {d}, but it was\n", .{expected_len});
+ } else {
+ std.debug.print("\n Expected length {d}, got {d}\n", .{ expected_len, actual_len });
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ /// Asserts that slice is empty.
+ pub fn beEmpty(self: Self) !void {
+ const matches = self.value.len == 0;
+ if (shouldFail(negated, matches)) {
+ if (negated) {
+ std.debug.print("\n Expected non-empty, but was empty\n", .{});
+ } else {
+ std.debug.print("\n Expected empty, got length {d}\n", .{self.value.len});
+ }
+ return error.ExpectationFailed;
+ }
+ }
+
+ // =========================================================
+ // Type Matchers
+ // =========================================================
+
+ /// Asserts that value is of expected type.
+ pub fn beOfType(self: Self, comptime ExpectedType: type) !void {
+ const matches = T == ExpectedType;
+ _ = self;
+ if (!matches) {
+ std.debug.print("\n Expected type {s}, got {s}\n", .{ @typeName(ExpectedType), @typeName(T) });
+ return error.ExpectationFailed;
+ }
+ }
+
+ /// Helper to determine if assertion should fail based on negation.
+ fn shouldFail(is_negated: bool, matches: bool) bool {
+ return (is_negated and matches) or (!is_negated and !matches);
+ }
+ };
+}
+
+// =========================================================================
+// Tests
+// =========================================================================
+
+test "expect().to().equal" {
+ try expect(@as(i32, 42)).to().equal(42);
+ // String equality requires .eql() for content comparison
+ try expect(@as([]const u8, "hello")).to().eql("hello");
+}
+
+test "expect().notTo().equal" {
+ try expect(@as(i32, 42)).notTo().equal(43);
+}
+
+test "expect().to().beTrue" {
+ try expect(true).to().beTrue();
+ try expect(5 > 3).to().beTrue();
+}
+
+test "expect().to().beFalse" {
+ try expect(false).to().beFalse();
+ try expect(3 > 5).to().beFalse();
+}
+
+test "expect().notTo().beTrue" {
+ try expect(false).notTo().beTrue();
+}
+
+test "expect().to().beNull" {
+ const value: ?i32 = null;
+ try expect(value).to().beNull();
+}
+
+test "expect().notTo().beNull" {
+ const value: ?i32 = 42;
+ try expect(value).notTo().beNull();
+}
+
+test "expect().to().beGreaterThan" {
+ try expect(@as(i32, 10)).to().beGreaterThan(5);
+ try expect(@as(i32, 5)).notTo().beGreaterThan(10);
+}
+
+test "expect().to().beLessThan" {
+ try expect(@as(i32, 5)).to().beLessThan(10);
+ try expect(@as(i32, 10)).notTo().beLessThan(5);
+}
+
+test "expect().to().beGreaterThanOrEqual" {
+ try expect(@as(i32, 10)).to().beGreaterThanOrEqual(10);
+ try expect(@as(i32, 10)).to().beGreaterThanOrEqual(5);
+ try expect(@as(i32, 5)).notTo().beGreaterThanOrEqual(10);
+}
+
+test "expect().to().beLessThanOrEqual" {
+ try expect(@as(i32, 10)).to().beLessThanOrEqual(10);
+ try expect(@as(i32, 5)).to().beLessThanOrEqual(10);
+ try expect(@as(i32, 10)).notTo().beLessThanOrEqual(5);
+}
+
+test "expect().to().beBetween" {
+ try expect(@as(i32, 5)).to().beBetween(1, 10);
+ try expect(@as(i32, 0)).notTo().beBetween(1, 10);
+}
+
+test "expect().to().contain" {
+ try expect(@as([]const u8, "hello world")).to().contain("world");
+ try expect(@as([]const u8, "hello world")).notTo().contain("foo");
+}
+
+test "expect().to().startWith" {
+ try expect(@as([]const u8, "hello world")).to().startWith("hello");
+ try expect(@as([]const u8, "hello world")).notTo().startWith("world");
+}
+
+test "expect().to().endWith" {
+ try expect(@as([]const u8, "hello world")).to().endWith("world");
+ try expect(@as([]const u8, "hello world")).notTo().endWith("hello");
+}
+
+test "expect().to().haveLength" {
+ try expect(@as([]const u8, "hello")).to().haveLength(5);
+ try expect(@as([]const u8, "hello")).notTo().haveLength(3);
+}
+
+test "expect().to().beEmpty" {
+ try expect(@as([]const u8, "")).to().beEmpty();
+ try expect(@as([]const u8, "hello")).notTo().beEmpty();
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/runner.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/runner.zig
new file mode 100644
index 0000000..90b6ccd
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/runner.zig
@@ -0,0 +1,798 @@
+//! ZSpec Test Runner
+//!
+//! Custom test runner that provides:
+//! - beforeAll/afterAll hooks (run once per scope)
+//! - before/after hooks (run before/after each test)
+//! - Scoped hooks that only apply to their containing struct
+//! - Colorized output
+//! - Slowest tests tracking
+
+const std = @import("std");
+const builtin = @import("builtin");
+const junit = @import("junit.zig");
+
+const Allocator = std.mem.Allocator;
+
+const BORDER = "=" ** 80;
+
+// Use in custom panic handler
+var current_test: ?[]const u8 = null;
+
+pub const std_options = std.Options{
+ .logFn = logging.log,
+ .log_level = .debug,
+};
+
+pub fn main() !void {
+ var mem: [8192]u8 = undefined;
+ var fba = std.heap.FixedBufferAllocator.init(&mem);
+
+ const allocator = fba.allocator();
+
+ // Use page allocator for JUnit writer (may need more memory for results)
+ const page_allocator = std.heap.page_allocator;
+
+ // Initialize `std.testing.io_instance` so tests that reach for
+ // `std.testing.io` get a real `Io.Threaded` with a worker pool.
+ //
+ // `std.testing` declares (testing.zig:34-35):
+ //
+ // pub var io_instance: Io.Threaded = undefined;
+ // pub const io = if (builtin.is_test) io_instance.io() else ...;
+ //
+ // `Threaded.io(t)` bakes `&io_instance` into the returned `Io`'s
+ // `userdata` *at comptime* (lib/std/Io/Threaded.zig:1806). Without
+ // a runtime init, the global memory at that address stays zero —
+ // worker_threads = null, etc. — and every operation that needs to
+ // enqueue work onto the thread pool deadlocks on linux. macOS
+ // happens to satisfy more zero-init paths along the way, so the
+ // bug stays hidden there and only manifests on linux.
+ //
+ // The stdlib's terminal runner does this re-init *per test* (see
+ // lib/compiler/test_runner.zig:273-282). For a `.simple`-mode
+ // runner a single global init is sufficient and cheaper than
+ // spinning up a worker pool for every test. See issue #44.
+ std.testing.io_instance = .init(std.testing.allocator, .{});
+ defer std.testing.io_instance.deinit();
+
+ const env = Env.init(allocator);
+ defer env.deinit(allocator);
+
+ var slowest = SlowTracker.init(allocator, 5);
+ defer slowest.deinit();
+
+ // Initialize JUnit writer if path is configured
+ var junit_writer: ?junit.JUnitWriter = if (env.junit_path != null)
+ junit.JUnitWriter.init(page_allocator, "zspec")
+ else
+ null;
+ defer if (junit_writer) |*jw| jw.deinit();
+
+ var pass: usize = 0;
+ var fail: usize = 0;
+ var skip: usize = 0;
+ var leak: usize = 0;
+
+ var printer = Printer.init(env.output_file);
+ defer printer.deinit();
+ printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line
+
+ // Track which scopes have had beforeAll run
+ var initialized_scopes: [64]?[]const u8 = .{null} ** 64;
+ var num_initialized_scopes: usize = 0;
+
+ const scopeInitialized = struct {
+ fn check(scopes: []const ?[]const u8, num: usize, scope: []const u8) bool {
+ for (scopes[0..num]) |s| {
+ if (s) |initialized| {
+ if (std.mem.eql(u8, initialized, scope)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }.check;
+
+ for (builtin.test_functions) |t| {
+ if (isHook(t)) {
+ continue;
+ }
+
+ var status = Status.pass;
+ slowest.startTiming();
+
+ const is_unnamed_test = isUnnamed(t);
+ if (env.filter) |f| {
+ if (!is_unnamed_test and std.mem.indexOf(u8, t.name, f) == null) {
+ continue;
+ }
+ }
+
+ // Handle skip_ prefixed tests
+ if (isSkipped(t)) {
+ skip += 1;
+ const skip_name = extractTestName(t.name);
+ if (env.verbose) {
+ printer.status(.skip, "{s} (skipped)\n", .{skip_name});
+ }
+ continue;
+ }
+
+ const friendly_name = blk: {
+ const name = t.name;
+ var it = std.mem.splitScalar(u8, name, '.');
+ while (it.next()) |value| {
+ if (std.mem.eql(u8, value, "test")) {
+ const rest = it.rest();
+ break :blk if (rest.len > 0) rest else name;
+ }
+ }
+ break :blk name;
+ };
+
+ // Run beforeAll hooks for scopes that haven't been initialized yet
+ for (builtin.test_functions) |hook| {
+ if (isSetup(hook)) {
+ const hook_scope = getScope(hook.name);
+ if (hookAppliesToTest(hook.name, t.name) and !scopeInitialized(&initialized_scopes, num_initialized_scopes, hook_scope)) {
+ hook.func() catch |err| {
+ printer.status(.fail, "\nbeforeAll \"{s}\" failed: {}\n", .{ hook.name, err });
+ status = .fail;
+ fail += 1;
+ };
+ if (num_initialized_scopes < initialized_scopes.len) {
+ initialized_scopes[num_initialized_scopes] = hook_scope;
+ num_initialized_scopes += 1;
+ }
+ }
+ }
+ }
+
+ current_test = friendly_name;
+ std.testing.allocator_instance = .{};
+
+ // Run before hooks that apply to this test's scope
+ for (builtin.test_functions) |hook| {
+ if (isBefore(hook) and hookAppliesToTest(hook.name, t.name)) {
+ hook.func() catch |err| {
+ printer.status(.fail, "\nbefore \"{s}\" failed: {}\n", .{ hook.name, err });
+ status = .fail;
+ fail += 1;
+ break;
+ };
+ }
+ }
+
+ const result = if (status == .fail) error.BeforeHookFailed else t.func();
+
+ // Run after hooks that apply to this test's scope (always run, even if test failed)
+ for (builtin.test_functions) |hook| {
+ if (isAfter(hook) and hookAppliesToTest(hook.name, t.name)) {
+ hook.func() catch |err| {
+ printer.status(.fail, "\nafter \"{s}\" failed: {}\n", .{ hook.name, err });
+ };
+ }
+ }
+
+ current_test = null;
+
+ const ns_taken = slowest.endTiming(friendly_name);
+
+ // Check for memory leaks if enabled
+ const leak_check = std.testing.allocator_instance.deinit();
+ if (env.detect_leaks and leak_check == .leak) {
+ leak += 1;
+ printer.status(.fail, "\n{s}\n\"{s}\" - Memory Leak Detected\n{s}\n", .{ BORDER, friendly_name, BORDER });
+ if (env.fail_on_leak) {
+ status = .fail;
+ fail += 1;
+ }
+ }
+
+ if (result) |_| {
+ pass += 1;
+ } else |err| switch (err) {
+ error.SkipZigTest => {
+ skip += 1;
+ status = .skip;
+ },
+ error.BeforeHookFailed => {
+ // Already handled above
+ },
+ else => {
+ status = .fail;
+ fail += 1;
+ printer.status(
+ .fail,
+ "\n{s}\n\"{s}\" - {s}\n{s}\n",
+ .{ BORDER, friendly_name, @errorName(err), BORDER },
+ );
+ if (@errorReturnTrace()) |trace| {
+ SmartStackTrace.dump(trace.*);
+ }
+ if (env.fail_first) {
+ break;
+ }
+ },
+ }
+
+ // Show test result based on verbose and failed_only settings
+ const should_show = if (env.failed_only)
+ status == .fail
+ else
+ env.verbose;
+
+ if (should_show) {
+ const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0;
+ printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms });
+ }
+
+ // Record result for JUnit XML output
+ if (junit_writer) |*jw| {
+ const junit_status: junit.TestResult.Status = switch (status) {
+ .pass => .passed,
+ .fail => .failed,
+ .skip => .skipped,
+ else => .passed,
+ };
+
+ const failure_message: ?[]const u8 = if (result) |_|
+ null
+ else |err| switch (err) {
+ error.SkipZigTest => null,
+ error.BeforeHookFailed => "before hook failed",
+ else => @errorName(err),
+ };
+
+ jw.addResult(.{
+ .name = friendly_name,
+ .classname = junit.extractClassname(t.name),
+ .time_ns = ns_taken,
+ .status = junit_status,
+ .failure_message = failure_message,
+ .failure_type = if (failure_message != null) "TestError" else null,
+ }) catch {};
+ }
+ }
+
+ // Run all afterAll hooks
+ for (builtin.test_functions) |t| {
+ if (isTeardown(t)) {
+ t.func() catch |err| {
+ printer.status(.fail, "\nafterAll \"{s}\" failed: {}\n", .{ t.name, err });
+ };
+ }
+ }
+
+ const total_tests = pass + fail;
+ const status = if (fail == 0) Status.pass else Status.fail;
+ printer.status(status, "\n{d} of {d} test{s} passed\n", .{ pass, total_tests, if (total_tests != 1) "s" else "" });
+ if (skip > 0) {
+ printer.status(.skip, "{d} test{s} skipped\n", .{ skip, if (skip != 1) "s" else "" });
+ }
+ if (leak > 0) {
+ printer.status(.fail, "{d} test{s} leaked\n", .{ leak, if (leak != 1) "s" else "" });
+ }
+ printer.fmt("\n", .{});
+ try slowest.display(printer);
+ printer.fmt("\n", .{});
+
+ // Write JUnit XML report if configured
+ if (junit_writer) |*jw| {
+ if (env.junit_path) |path| {
+ jw.writeToFile(path) catch |err| {
+ printer.status(.fail, "Failed to write JUnit XML to {s}: {}\n", .{ path, err });
+ };
+ printer.fmt("JUnit XML report written to: {s}\n", .{path});
+ }
+ }
+
+ // Exit with failure if tests failed or if leaks detected with fail_on_leak enabled
+ const should_fail = fail > 0 or (env.fail_on_leak and leak > 0);
+ std.process.exit(if (should_fail) 1 else 0);
+}
+
+const Printer = struct {
+ // `std.fs.File` moved to `std.Io.File` in 0.16 and requires an `Io` to
+ // operate. To keep the runner self-contained, the POSIX path uses raw libc
+ // and the Windows path uses Win32 directly (since `std.c.O` is `void` on
+ // Windows in 0.16).
+ handle: ?FileHandle,
+
+ const FileHandle = if (builtin.os.tag == .windows) std.os.windows.HANDLE else std.c.fd_t;
+
+ fn init(output_path: ?[]const u8) Printer {
+ const handle: ?FileHandle = if (output_path) |path| openForWrite(path) else null;
+ return .{ .handle = handle };
+ }
+
+ fn openForWrite(path: []const u8) ?FileHandle {
+ if (builtin.os.tag == .windows) {
+ const w = std.os.windows;
+ const k32 = struct {
+ extern "kernel32" fn CreateFileW(
+ lpFileName: w.LPCWSTR,
+ dwDesiredAccess: w.DWORD,
+ dwShareMode: w.DWORD,
+ lpSecurityAttributes: ?*anyopaque,
+ dwCreationDisposition: w.DWORD,
+ dwFlagsAndAttributes: w.DWORD,
+ hTemplateFile: ?w.HANDLE,
+ ) callconv(.winapi) w.HANDLE;
+ };
+ const GENERIC_WRITE: w.DWORD = 0x40000000;
+ const CREATE_ALWAYS: w.DWORD = 2;
+ const FILE_ATTRIBUTE_NORMAL: w.DWORD = 0x80;
+
+ var path_buf_w: [std.fs.max_path_bytes]u16 = undefined;
+ const path_len_w = std.unicode.wtf8ToWtf16Le(&path_buf_w, path) catch return null;
+ if (path_len_w >= path_buf_w.len) return null;
+ path_buf_w[path_len_w] = 0;
+ const path_z: w.LPCWSTR = @ptrCast(&path_buf_w);
+
+ const h = k32.CreateFileW(path_z, GENERIC_WRITE, 0, null, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, null);
+ if (h == w.INVALID_HANDLE_VALUE) return null;
+ return h;
+ } else {
+ var buf: [std.fs.max_path_bytes:0]u8 = undefined;
+ if (path.len >= buf.len) return null;
+ @memcpy(buf[0..path.len], path);
+ buf[path.len] = 0;
+ const flags: std.c.O = .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true };
+ const opened = std.c.open(@ptrCast(&buf), flags, @as(std.c.mode_t, 0o644));
+ if (opened < 0) return null;
+ return opened;
+ }
+ }
+
+ fn deinit(self: *Printer) void {
+ if (self.handle) |h| {
+ if (builtin.os.tag == .windows) {
+ std.os.windows.CloseHandle(h);
+ } else {
+ _ = std.c.close(h);
+ }
+ }
+ }
+
+ fn writeHandle(handle: FileHandle, bytes: []const u8) void {
+ if (builtin.os.tag == .windows) {
+ const w = std.os.windows;
+ const k32 = struct {
+ extern "kernel32" fn WriteFile(
+ hFile: w.HANDLE,
+ lpBuffer: [*]const u8,
+ nNumberOfBytesToWrite: w.DWORD,
+ lpNumberOfBytesWritten: *w.DWORD,
+ lpOverlapped: ?*anyopaque,
+ ) callconv(.winapi) w.BOOL;
+ };
+ var remaining = bytes;
+ while (remaining.len > 0) {
+ const chunk_len: w.DWORD = @intCast(@min(remaining.len, std.math.maxInt(w.DWORD)));
+ var written: w.DWORD = 0;
+ const ok = k32.WriteFile(handle, remaining.ptr, chunk_len, &written, null);
+ if (!ok.toBool() or written == 0) return;
+ remaining = remaining[written..];
+ }
+ } else {
+ var remaining = bytes;
+ while (remaining.len > 0) {
+ const n = std.c.write(handle, remaining.ptr, remaining.len);
+ if (n <= 0) return;
+ remaining = remaining[@intCast(n)..];
+ }
+ }
+ }
+
+ fn fmt(self: Printer, comptime format: []const u8, args: anytype) void {
+ std.debug.print(format, args);
+ // Write to file, stripping ANSI escape codes
+ if (self.handle) |h| {
+ var buf: [4096]u8 = undefined;
+ const output = std.fmt.bufPrint(&buf, format, args) catch return;
+ // Skip if it's just ANSI control sequences (starts with \x1b or \r)
+ if (output.len > 0 and (output[0] == '\x1b' or output[0] == '\r')) {
+ return;
+ }
+ writeHandle(h, output);
+ }
+ }
+
+ fn status(self: Printer, s: Status, comptime format: []const u8, args: anytype) void {
+ const color = switch (s) {
+ .pass => "\x1b[32m",
+ .fail => "\x1b[31m",
+ .skip => "\x1b[33m",
+ else => "",
+ };
+ std.debug.print("{s}", .{color});
+ std.debug.print(format, args);
+ std.debug.print("\x1b[0m", .{});
+
+ // Write to file without ANSI escape codes
+ if (self.handle) |h| {
+ var buf: [4096]u8 = undefined;
+ const output = std.fmt.bufPrint(&buf, format, args) catch return;
+ writeHandle(h, output);
+ }
+ }
+
+};
+
+const Status = enum {
+ pass,
+ fail,
+ skip,
+ text,
+};
+
+/// Monotonic timer replacement for `std.time.Timer` (removed in Zig 0.16).
+/// Reads the monotonic clock directly via `std.posix.clock_gettime` on POSIX
+/// targets and `QueryPerformanceCounter` on Windows.
+const MonoTimer = struct {
+ started_ns: u64,
+
+ fn nowNanos() u64 {
+ const native_os = @import("builtin").os.tag;
+ switch (native_os) {
+ .windows => {
+ // `std.os.windows.QueryPerformance*` were removed in 0.16; bind
+ // the syscalls directly.
+ const w = std.os.windows;
+ const k32 = struct {
+ extern "kernel32" fn QueryPerformanceCounter(lpPerformanceCount: *w.LARGE_INTEGER) callconv(.winapi) w.BOOL;
+ extern "kernel32" fn QueryPerformanceFrequency(lpFrequency: *w.LARGE_INTEGER) callconv(.winapi) w.BOOL;
+ };
+ var ticks_li: w.LARGE_INTEGER = 0;
+ var freq_li: w.LARGE_INTEGER = 0;
+ _ = k32.QueryPerformanceCounter(&ticks_li);
+ _ = k32.QueryPerformanceFrequency(&freq_li);
+ const ticks: u64 = @intCast(ticks_li);
+ const freq: u64 = @intCast(freq_li);
+ // Convert ticks -> nanoseconds without overflowing.
+ const ns_per_s: u64 = std.time.ns_per_s;
+ const seconds: u64 = ticks / freq;
+ const remainder: u64 = ticks % freq;
+ return seconds * ns_per_s + (remainder * ns_per_s) / freq;
+ },
+ else => {
+ var ts: std.posix.timespec = undefined;
+ _ = std.posix.system.clock_gettime(.MONOTONIC, &ts);
+ const sec: u64 = @intCast(ts.sec);
+ const nsec: u64 = @intCast(ts.nsec);
+ return sec * std.time.ns_per_s + nsec;
+ },
+ }
+ }
+
+ fn start() MonoTimer {
+ return .{ .started_ns = nowNanos() };
+ }
+
+ fn reset(self: *MonoTimer) void {
+ self.started_ns = nowNanos();
+ }
+
+ fn lap(self: *MonoTimer) u64 {
+ const now_ns = nowNanos();
+ const elapsed = now_ns -% self.started_ns;
+ self.started_ns = now_ns;
+ return elapsed;
+ }
+};
+
+const SlowTracker = struct {
+ const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming);
+ allocator: Allocator,
+ max: usize,
+ slowest: SlowestQueue,
+ timer: MonoTimer,
+
+ fn init(alloc: Allocator, count: u32) SlowTracker {
+ const timer = MonoTimer.start();
+ var slow: SlowestQueue = .empty;
+ slow.ensureTotalCapacity(alloc, count) catch @panic("OOM");
+ return .{
+ .allocator = alloc,
+ .max = count,
+ .timer = timer,
+ .slowest = slow,
+ };
+ }
+
+ const TestInfo = struct {
+ ns: u64,
+ name: []const u8,
+ };
+
+ fn deinit(self: *SlowTracker) void {
+ self.slowest.deinit(self.allocator);
+ }
+
+ fn startTiming(self: *SlowTracker) void {
+ self.timer.reset();
+ }
+
+ fn endTiming(self: *SlowTracker, test_name: []const u8) u64 {
+ var timer = self.timer;
+ const ns = timer.lap();
+
+ var slow = &self.slowest;
+
+ if (slow.count() < self.max) {
+ slow.push(self.allocator, TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
+ return ns;
+ }
+
+ {
+ const fastest_of_the_slow = slow.peekMin() orelse unreachable;
+ if (fastest_of_the_slow.ns > ns) {
+ return ns;
+ }
+ }
+
+ _ = slow.popMin();
+ slow.push(self.allocator, TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
+ return ns;
+ }
+
+ fn display(self: *SlowTracker, printer: Printer) !void {
+ var slow = self.slowest;
+ const count = slow.count();
+ printer.fmt("Slowest {d} test{s}: \n", .{ count, if (count != 1) "s" else "" });
+ while (slow.popMin()) |info| {
+ const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0;
+ printer.fmt(" {d:.2}ms\t{s}\n", .{ ms, info.name });
+ }
+ }
+
+ fn compareTiming(_: void, a: TestInfo, b: TestInfo) std.math.Order {
+ return std.math.order(a.ns, b.ns);
+ }
+};
+
+const Env = struct {
+ verbose: bool,
+ fail_first: bool,
+ filter: ?[]const u8,
+ junit_path: ?[]const u8,
+ detect_leaks: bool,
+ fail_on_leak: bool,
+ failed_only: bool,
+ output_file: ?[]const u8,
+
+ fn init(alloc: Allocator) Env {
+ return .{
+ .verbose = readEnvBool(alloc, "TEST_VERBOSE", true),
+ .fail_first = readEnvBool(alloc, "TEST_FAIL_FIRST", false),
+ .filter = readEnv(alloc, "TEST_FILTER"),
+ .junit_path = readEnv(alloc, "TEST_JUNIT_PATH"),
+ .detect_leaks = readEnvBool(alloc, "TEST_DETECT_LEAKS", true),
+ .fail_on_leak = readEnvBool(alloc, "TEST_FAIL_ON_LEAK", true),
+ .failed_only = readEnvBool(alloc, "TEST_FAILED_ONLY", false),
+ .output_file = readEnv(alloc, "TEST_OUTPUT_FILE"),
+ };
+ }
+
+ fn deinit(self: Env, alloc: Allocator) void {
+ if (self.filter) |f| {
+ alloc.free(f);
+ }
+ if (self.junit_path) |p| {
+ alloc.free(p);
+ }
+ if (self.output_file) |f| {
+ alloc.free(f);
+ }
+ }
+
+ fn readEnv(alloc: Allocator, key: []const u8) ?[]const u8 {
+ // `std.process.getEnvVarOwned` was removed in Zig 0.16. For a custom test
+ // runner we don't have an `Io` to feed `Environ.getAlloc`, so we read
+ // straight from libc's `getenv` (POSIX) or the Windows API directly
+ // (the `kernel32.GetEnvironmentVariableW` wrapper was dropped in 0.16).
+ const native_os = @import("builtin").os.tag;
+ switch (native_os) {
+ .windows => {
+ const w = std.os.windows;
+ const k32 = struct {
+ extern "kernel32" fn GetEnvironmentVariableW(
+ lpName: w.LPCWSTR,
+ lpBuffer: ?[*]u16,
+ nSize: w.DWORD,
+ ) callconv(.winapi) w.DWORD;
+ };
+ // Convert key to WTF-16, query, and convert back.
+ var key_buf_w: [256]u16 = undefined;
+ const key_len_w = std.unicode.wtf8ToWtf16Le(&key_buf_w, key) catch return null;
+ if (key_len_w >= key_buf_w.len) return null;
+ key_buf_w[key_len_w] = 0;
+ const key_z: w.LPCWSTR = @ptrCast(&key_buf_w);
+
+ var val_buf_w: [4096]u16 = undefined;
+ const written = k32.GetEnvironmentVariableW(key_z, &val_buf_w, val_buf_w.len);
+ if (written == 0) {
+ // `GetEnvironmentVariableW` returns 0 both when the var is
+ // missing AND when it exists with an empty value. Disambiguate
+ // via `GetLastError`: only `ERROR_ENVVAR_NOT_FOUND` is truly
+ // "not present" (return null). An empty existing value should
+ // be returned as a zero-length owned slice, matching the
+ // POSIX `getenv` semantics where an empty string is present.
+ const err = w.GetLastError();
+ if (err == .ENVVAR_NOT_FOUND) return null;
+ return alloc.dupe(u8, "") catch null;
+ }
+ if (written >= val_buf_w.len) return null;
+ const wtf16 = val_buf_w[0..written];
+ return std.unicode.wtf16LeToWtf8Alloc(alloc, wtf16) catch null;
+ },
+ else => {
+ // libc `getenv` requires a null-terminated key.
+ var key_buf: [256]u8 = undefined;
+ if (key.len >= key_buf.len) return null;
+ @memcpy(key_buf[0..key.len], key);
+ key_buf[key.len] = 0;
+ const c_value = std.c.getenv(@ptrCast(&key_buf)) orelse return null;
+ const span = std.mem.span(c_value);
+ return alloc.dupe(u8, span) catch null;
+ },
+ }
+ }
+
+ fn readEnvBool(alloc: Allocator, key: []const u8, deflt: bool) bool {
+ const value = readEnv(alloc, key) orelse return deflt;
+ defer alloc.free(value);
+ return std.ascii.eqlIgnoreCase(value, "true");
+ }
+};
+
+pub const panic = std.debug.FullPanic(struct {
+ pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn {
+ if (current_test) |ct| {
+ std.debug.print("\x1b[31m{s}\npanic running \"{s}\"\n{s}\x1b[0m\n", .{ BORDER, ct, BORDER });
+ }
+ std.debug.defaultPanic(msg, first_trace_addr);
+ }
+}.panicFn);
+
+fn isUnnamed(t: std.builtin.TestFn) bool {
+ const marker = ".test_";
+ const test_name = t.name;
+ const index = std.mem.indexOf(u8, test_name, marker) orelse return false;
+ _ = std.fmt.parseInt(u32, test_name[index + marker.len ..], 10) catch return false;
+ return true;
+}
+
+fn isSetup(t: std.builtin.TestFn) bool {
+ return std.mem.endsWith(u8, t.name, "tests:beforeAll");
+}
+
+fn isTeardown(t: std.builtin.TestFn) bool {
+ return std.mem.endsWith(u8, t.name, "tests:afterAll");
+}
+
+fn isBefore(t: std.builtin.TestFn) bool {
+ return std.mem.endsWith(u8, t.name, "tests:before");
+}
+
+fn isAfter(t: std.builtin.TestFn) bool {
+ return std.mem.endsWith(u8, t.name, "tests:after");
+}
+
+fn isHook(t: std.builtin.TestFn) bool {
+ return isSetup(t) or isTeardown(t) or isBefore(t) or isAfter(t);
+}
+
+fn isSkipped(t: std.builtin.TestFn) bool {
+ // Check if the test name contains "skip_" after the ".test." marker
+ if (std.mem.indexOf(u8, t.name, ".test.skip_")) |_| {
+ return true;
+ }
+ return false;
+}
+
+fn extractTestName(name: []const u8) []const u8 {
+ var it = std.mem.splitScalar(u8, name, '.');
+ while (it.next()) |value| {
+ if (std.mem.eql(u8, value, "test")) {
+ const rest = it.rest();
+ return if (rest.len > 0) rest else name;
+ }
+ }
+ return name;
+}
+
+fn getScope(name: []const u8) []const u8 {
+ if (std.mem.indexOf(u8, name, ".test.")) |idx| {
+ return name[0..idx];
+ }
+ if (std.mem.indexOf(u8, name, ".test_")) |idx| {
+ return name[0..idx];
+ }
+ return name;
+}
+
+fn hookAppliesToTest(hook_name: []const u8, test_name: []const u8) bool {
+ const hook_scope = getScope(hook_name);
+ const test_scope = getScope(test_name);
+ return std.mem.startsWith(u8, test_scope, hook_scope);
+}
+
+const logging = struct {
+ pub fn log(
+ comptime _: std.log.Level,
+ comptime _: @TypeOf(.enum_literal),
+ comptime _: []const u8,
+ _: anytype,
+ ) void {}
+};
+
+/// Smart stack trace that filters out framework frames and shows source context
+const SmartStackTrace = struct {
+ fn dump(trace: std.builtin.StackTrace) void {
+ std.debug.print("\n\x1b[1mStack trace:\x1b[0m\n", .{});
+
+ // Zig 0.16 split `std.builtin.StackTrace` (handed to us by failing
+ // tests) from `std.debug.StackTrace` (consumed by `dumpStackTrace`).
+ // Build the debug variant before dumping.
+ const valid_addrs = trace.instruction_addresses[0..@min(trace.index, trace.instruction_addresses.len)];
+ const debug_trace: std.debug.StackTrace = .{
+ .return_addresses = valid_addrs,
+ .skipped = .none,
+ };
+ std.debug.dumpStackTrace(&debug_trace);
+
+ // The Zig 0.15 source-context viewer relied on
+ // `std.debug.SelfInfo.getModuleForAddress` plus `std.fs.cwd().openFile`,
+ // both of which were reworked in 0.16. The default stack trace already
+ // prints source lines, so we no longer duplicate that here.
+ }
+
+ /// Checks if a stack frame is from framework code (runner, expect, zspec, std lib).
+ /// Returns true for framework frames that should be filtered out of user-facing traces.
+ pub fn isFrameworkFrame(file_name: []const u8) bool {
+ // Filter out zspec internals
+ if (std.mem.indexOf(u8, file_name, "runner.zig")) |_| return true;
+ if (std.mem.indexOf(u8, file_name, "zspec.zig")) |_| return true;
+ if (std.mem.indexOf(u8, file_name, "expect.zig")) |_| return true;
+ // Filter out std library internals
+ if (std.mem.indexOf(u8, file_name, "/zig/lib/")) |_| return true;
+ return false;
+ }
+
+};
+
+// Unit tests for SmartStackTrace
+test "isFrameworkFrame identifies runner.zig as framework" {
+ try std.testing.expect(SmartStackTrace.isFrameworkFrame("/path/to/src/runner.zig"));
+ try std.testing.expect(SmartStackTrace.isFrameworkFrame("runner.zig"));
+}
+
+test "isFrameworkFrame identifies zspec.zig as framework" {
+ try std.testing.expect(SmartStackTrace.isFrameworkFrame("/path/to/src/zspec.zig"));
+ try std.testing.expect(SmartStackTrace.isFrameworkFrame("zspec.zig"));
+}
+
+test "isFrameworkFrame identifies expect.zig as framework" {
+ try std.testing.expect(SmartStackTrace.isFrameworkFrame("/path/to/src/expect.zig"));
+ try std.testing.expect(SmartStackTrace.isFrameworkFrame("expect.zig"));
+}
+
+test "isFrameworkFrame identifies std library as framework" {
+ try std.testing.expect(SmartStackTrace.isFrameworkFrame("/usr/lib/zig/lib/std/testing.zig"));
+ try std.testing.expect(SmartStackTrace.isFrameworkFrame("/home/user/.zig/lib/std.zig"));
+}
+
+test "isFrameworkFrame returns false for user test files" {
+ try std.testing.expect(!SmartStackTrace.isFrameworkFrame("/project/tests/my_test.zig"));
+ try std.testing.expect(!SmartStackTrace.isFrameworkFrame("/project/src/calculator.zig"));
+ try std.testing.expect(!SmartStackTrace.isFrameworkFrame("user_code.zig"));
+}
+
+test "isFrameworkFrame returns false for user files with similar names" {
+ // Should not match partial names
+ try std.testing.expect(!SmartStackTrace.isFrameworkFrame("/project/my_runner_test.zig"));
+ try std.testing.expect(!SmartStackTrace.isFrameworkFrame("/project/expect_helper.zig"));
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/zspec.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/zspec.zig
new file mode 100644
index 0000000..f1b27d4
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/src/zspec.zig
@@ -0,0 +1,401 @@
+//! ZSpec - RSpec-like testing framework for Zig
+//!
+//! Provides:
+//! - describe/context blocks via nested structs
+//! - before/after hooks (per-test)
+//! - beforeAll/afterAll hooks (per-scope)
+//! - let (memoized lazy values)
+//! - Custom matchers and assertions
+//! - Factory (FactoryBot-like test data generation)
+
+const std = @import("std");
+const builtin = @import("builtin");
+
+// Re-export Factory module
+pub const Factory = @import("factory.zig");
+
+// Re-export Fixture module
+pub const Fixture = @import("fixture.zig");
+
+// Re-export fluent matchers module
+pub const matchers = @import("matchers.zig");
+/// Fluent expect function: try expectFluent(value).to().equal(expected)
+pub const expectFluent = matchers.expect;
+
+/// Memoized lazy value that is computed once per test and cached.
+/// Similar to RSpec's `let`.
+pub fn Let(comptime T: type, comptime init_fn: fn () T) type {
+ return struct {
+ var cached_value: ?T = null;
+ var initialized: bool = false;
+
+ pub fn get() T {
+ if (!initialized) {
+ cached_value = init_fn();
+ initialized = true;
+ }
+ return cached_value.?;
+ }
+
+ pub fn reset() void {
+ cached_value = null;
+ initialized = false;
+ }
+ };
+}
+
+/// Memoized lazy value with allocator support for heap allocations.
+pub fn LetAlloc(comptime T: type, comptime init_fn: fn (std.mem.Allocator) T) type {
+ return struct {
+ var cached_value: ?T = null;
+ var initialized: bool = false;
+
+ pub fn get(alloc: std.mem.Allocator) T {
+ if (!initialized) {
+ cached_value = init_fn(alloc);
+ initialized = true;
+ }
+ return cached_value.?;
+ }
+
+ pub fn reset() void {
+ cached_value = null;
+ initialized = false;
+ }
+ };
+}
+
+/// Comparison helper for `expect.equal` / `expect.notEqual`. Dispatches
+/// on `@typeInfo` so types that don't support `==` (slices, error unions)
+/// still compare correctly:
+///
+/// - Slices compare element-wise via `std.mem.eql` so `[]const u8`
+/// equality "just works" without forcing callers into
+/// `std.testing.expectEqualStrings`.
+/// - Error unions compare by resolving both sides: same error → equal,
+/// same payload (recursively) → equal, mismatched outcomes → not equal.
+/// - Everything else falls through to plain `==`, preserving the
+/// existing behavior for primitives, enums, simple structs, etc.
+fn valuesEqual(actual: anytype, expected: @TypeOf(actual)) bool {
+ const T = @TypeOf(actual);
+ switch (@typeInfo(T)) {
+ .pointer => |p| {
+ if (p.size == .slice) return std.mem.eql(p.child, actual, expected);
+ return actual == expected;
+ },
+ .error_union => {
+ // Resolve both sides into either an error or a payload, then
+ // compare the matching variants. Using `if (x) |v| ... else |e| ...`
+ // (rather than `x catch |e| e`) is the only way to extract a
+ // bare error-set value from an error union without dragging
+ // the union back into the result type.
+ if (actual) |a_val| {
+ const e_val = expected catch return false;
+ return valuesEqual(a_val, e_val);
+ } else |a_err| {
+ if (expected) |_| {
+ return false;
+ } else |e_err| {
+ return a_err == e_err;
+ }
+ }
+ },
+ else => return actual == expected,
+ }
+}
+
+/// Custom expectation/matcher system
+pub const expect = struct {
+ pub fn equal(actual: anytype, expected: @TypeOf(actual)) !void {
+ if (!valuesEqual(actual, expected)) {
+ std.debug.print("\n Expected: {any}\n Actual: {any}\n", .{ expected, actual });
+ return error.ExpectationFailed;
+ }
+ }
+
+ pub fn notEqual(actual: anytype, expected: @TypeOf(actual)) !void {
+ if (valuesEqual(actual, expected)) {
+ std.debug.print("\n Expected {any} to not equal {any}\n", .{ actual, expected });
+ return error.ExpectationFailed;
+ }
+ }
+
+ /// Assert that an error-union value resolved to a specific error.
+ /// Mirrors `std.testing.expectError` with zspec's stderr formatting.
+ /// Use this instead of `expect.equal(result, error.Foo)` — `equal`
+ /// can compare error unions, but `toReturnError` reads better at the
+ /// call site for "this should have errored" assertions.
+ pub fn toReturnError(actual: anytype, expected: anyerror) !void {
+ const T = @TypeOf(actual);
+ if (@typeInfo(T) != .error_union)
+ @compileError("expect.toReturnError requires an error-union value, got " ++ @typeName(T));
+
+ if (actual) |_| {
+ std.debug.print("\n Expected error.{s}, but got a value\n", .{@errorName(expected)});
+ return error.ExpectationFailed;
+ } else |actual_err| {
+ if (actual_err != expected) {
+ std.debug.print("\n Expected error.{s}, got error.{s}\n", .{ @errorName(expected), @errorName(actual_err) });
+ return error.ExpectationFailed;
+ }
+ }
+ }
+
+ pub fn toBeTrue(actual: bool) !void {
+ if (!actual) {
+ std.debug.print("\n Expected true, got false\n", .{});
+ return error.ExpectationFailed;
+ }
+ }
+
+ pub fn toBeFalse(actual: bool) !void {
+ if (actual) {
+ std.debug.print("\n Expected false, got true\n", .{});
+ return error.ExpectationFailed;
+ }
+ }
+
+ pub fn toBeNull(actual: anytype) !void {
+ if (actual != null) {
+ std.debug.print("\n Expected null, got {any}\n", .{actual});
+ return error.ExpectationFailed;
+ }
+ }
+
+ pub fn notToBeNull(actual: anytype) !void {
+ if (actual == null) {
+ std.debug.print("\n Expected non-null value, got null\n", .{});
+ return error.ExpectationFailed;
+ }
+ }
+
+ pub fn toBeGreaterThan(actual: anytype, expected: @TypeOf(actual)) !void {
+ if (actual <= expected) {
+ std.debug.print("\n Expected {any} > {any}\n", .{ actual, expected });
+ return error.ExpectationFailed;
+ }
+ }
+
+ pub fn toBeLessThan(actual: anytype, expected: @TypeOf(actual)) !void {
+ if (actual >= expected) {
+ std.debug.print("\n Expected {any} < {any}\n", .{ actual, expected });
+ return error.ExpectationFailed;
+ }
+ }
+
+ pub fn toContain(haystack: []const u8, needle: []const u8) !void {
+ if (std.mem.indexOf(u8, haystack, needle) == null) {
+ std.debug.print("\n Expected \"{s}\" to contain \"{s}\"\n", .{ haystack, needle });
+ return error.ExpectationFailed;
+ }
+ }
+
+ pub fn toHaveLength(slice: anytype, expected_len: usize) !void {
+ const actual_len = slice.len;
+ if (actual_len != expected_len) {
+ std.debug.print("\n Expected length {d}, got {d}\n", .{ expected_len, actual_len });
+ return error.ExpectationFailed;
+ }
+ }
+
+ pub fn toBeEmpty(slice: anytype) !void {
+ if (slice.len != 0) {
+ std.debug.print("\n Expected empty, got length {d}\n", .{slice.len});
+ return error.ExpectationFailed;
+ }
+ }
+
+ pub fn notToBeEmpty(slice: anytype) !void {
+ if (slice.len == 0) {
+ std.debug.print("\n Expected non-empty slice\n", .{});
+ return error.ExpectationFailed;
+ }
+ }
+};
+
+/// Describes a test suite. Use with nested structs for organization.
+/// This is mainly for documentation - the actual structure comes from nested pub const structs.
+pub fn describe(comptime name: []const u8, comptime T: type) type {
+ _ = name; // Name is embedded in the struct for the test runner to discover
+ return T;
+}
+
+/// Alias for describe - used for sub-contexts
+pub const context = describe;
+
+/// Helper to run all tests in a spec struct
+pub fn runAll(comptime T: type) void {
+ refAllDeclsRecursive(T);
+}
+
+/// Local replacement for the removed `std.testing.refAllDeclsRecursive`.
+/// Recursively references every declaration so nested test blocks are discovered.
+fn refAllDeclsRecursive(comptime T: type) void {
+ if (!@import("builtin").is_test) return;
+ const info = @typeInfo(T);
+ switch (info) {
+ .@"struct", .@"enum", .@"union", .@"opaque" => {
+ inline for (comptime std.meta.declarations(T)) |decl| {
+ if (@TypeOf(@field(T, decl.name)) == type) {
+ switch (@typeInfo(@field(T, decl.name))) {
+ .@"struct", .@"enum", .@"union", .@"opaque" => refAllDeclsRecursive(@field(T, decl.name)),
+ else => {},
+ }
+ }
+ _ = &@field(T, decl.name);
+ }
+ },
+ else => {},
+ }
+}
+
+// Re-export testing allocator for convenience
+pub const allocator = std.testing.allocator;
+
+test "Let memoization" {
+ var call_count: usize = 0;
+
+ const TestLet = struct {
+ var counter: *usize = undefined;
+
+ fn init() i32 {
+ counter.* += 1;
+ return 42;
+ }
+ };
+ TestLet.counter = &call_count;
+
+ const value = Let(i32, TestLet.init);
+
+ // First call should initialize
+ try std.testing.expectEqual(42, value.get());
+ try std.testing.expectEqual(1, call_count);
+
+ // Second call should return cached value
+ try std.testing.expectEqual(42, value.get());
+ try std.testing.expectEqual(1, call_count);
+
+ // Reset and call again
+ value.reset();
+ try std.testing.expectEqual(42, value.get());
+ try std.testing.expectEqual(2, call_count);
+}
+
+test "expect.toHaveLength" {
+ const arr = [_]i32{ 1, 2, 3 };
+ try expect.toHaveLength(&arr, 3);
+}
+
+// ── expect.equal / notEqual / toReturnError on slices and error unions ──
+//
+// Pre-#40 these failed at compile time with
+// "operator != not allowed for type '[]const u8'" / "...error union".
+// The tests below pin the smart-dispatch in valuesEqual + the new
+// toReturnError matcher.
+
+test "expect.equal: slice of u8 (string) — equal contents" {
+ try expect.equal(@as([]const u8, "hello"), "hello");
+}
+
+test "expect.equal: slice of u8 — same length, different bytes" {
+ try std.testing.expectError(
+ error.ExpectationFailed,
+ expect.equal(@as([]const u8, "hello"), "world"),
+ );
+}
+
+test "expect.equal: slice of u8 — different length" {
+ try std.testing.expectError(
+ error.ExpectationFailed,
+ expect.equal(@as([]const u8, "hi"), "hello"),
+ );
+}
+
+test "expect.equal: slice of i32" {
+ const a = [_]i32{ 1, 2, 3 };
+ const b = [_]i32{ 1, 2, 3 };
+ try expect.equal(@as([]const i32, &a), @as([]const i32, &b));
+}
+
+test "expect.notEqual: slice of u8 — different contents" {
+ try expect.notEqual(@as([]const u8, "hello"), "world");
+}
+
+test "expect.notEqual: slice of u8 — equal contents fails" {
+ try std.testing.expectError(
+ error.ExpectationFailed,
+ expect.notEqual(@as([]const u8, "hello"), "hello"),
+ );
+}
+
+test "expect.equal: error union — both same error" {
+ const Result = error{Foo}!u32;
+ const a: Result = error.Foo;
+ const b: Result = error.Foo;
+ try expect.equal(a, b);
+}
+
+test "expect.equal: error union — both same payload" {
+ const Result = error{Foo}!u32;
+ const a: Result = 42;
+ const b: Result = 42;
+ try expect.equal(a, b);
+}
+
+test "expect.equal: error union — error vs payload fails" {
+ const Result = error{Foo}!u32;
+ const a: Result = error.Foo;
+ const b: Result = 42;
+ try std.testing.expectError(error.ExpectationFailed, expect.equal(a, b));
+}
+
+test "expect.equal: error union — different errors fail" {
+ const Result = error{ Foo, Bar }!u32;
+ const a: Result = error.Foo;
+ const b: Result = error.Bar;
+ try std.testing.expectError(error.ExpectationFailed, expect.equal(a, b));
+}
+
+test "expect.equal: error union with slice payload — recurses" {
+ const Result = error{Foo}![]const u8;
+ const a: Result = "hello";
+ const b: Result = "hello";
+ try expect.equal(a, b);
+}
+
+test "expect.toReturnError: matches expected error" {
+ const Result = error{Foo}!u32;
+ const a: Result = error.Foo;
+ try expect.toReturnError(a, error.Foo);
+}
+
+test "expect.toReturnError: mismatch fails" {
+ const Result = error{ Foo, Bar }!u32;
+ const a: Result = error.Foo;
+ try std.testing.expectError(
+ error.ExpectationFailed,
+ expect.toReturnError(a, error.Bar),
+ );
+}
+
+test "expect.toReturnError: payload-instead-of-error fails" {
+ const Result = error{Foo}!u32;
+ const a: Result = 42;
+ try std.testing.expectError(
+ error.ExpectationFailed,
+ expect.toReturnError(a, error.Foo),
+ );
+}
+
+// Sanity check: the existing non-slice non-error path still works.
+test "expect.equal: int (regression check for the dispatch fall-through)" {
+ try expect.equal(@as(u32, 42), 42);
+ try std.testing.expectError(error.ExpectationFailed, expect.equal(@as(u32, 1), @as(u32, 2)));
+}
+
+// Include tests from submodules
+test {
+ _ = @import("factory.zig");
+ _ = @import("fixture.zig");
+ _ = @import("matchers.zig");
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/example_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/example_test.zig
new file mode 100644
index 0000000..688a449
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/example_test.zig
@@ -0,0 +1,225 @@
+//! Example tests demonstrating ZSpec features
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+const allocator = zspec.allocator;
+
+test {
+ zspec.runAll(@This());
+}
+
+// Top-level hooks apply to all tests
+var total_tests: usize = 0;
+
+test "tests:beforeAll" {
+ total_tests = 0;
+ std.debug.print("\n[Calculator Tests] Starting...\n", .{});
+}
+
+test "tests:afterAll" {
+ std.debug.print("[Calculator Tests] Completed {d} tests\n", .{total_tests});
+}
+
+test "tests:before" {
+ total_tests += 1;
+}
+
+// Example: Simple Calculator
+const Calculator = struct {
+ value: i32,
+
+ pub fn init() Calculator {
+ return .{ .value = 0 };
+ }
+
+ pub fn add(self: *Calculator, n: i32) void {
+ self.value += n;
+ }
+
+ pub fn subtract(self: *Calculator, n: i32) void {
+ self.value -= n;
+ }
+
+ pub fn multiply(self: *Calculator, n: i32) void {
+ self.value *= n;
+ }
+
+ pub fn reset(self: *Calculator) void {
+ self.value = 0;
+ }
+};
+
+pub const ADD = struct {
+ var calc: Calculator = undefined;
+
+ test "tests:before" {
+ calc = Calculator.init();
+ }
+
+ test "adds positive numbers" {
+ calc.add(5);
+ try expect.equal(calc.value, 5);
+ }
+
+ test "adds negative numbers" {
+ calc.add(-3);
+ try expect.equal(calc.value, -3);
+ }
+
+ test "adds zero" {
+ calc.add(0);
+ try expect.equal(calc.value, 0);
+ }
+
+ test "adds multiple times" {
+ calc.add(10);
+ calc.add(20);
+ calc.add(30);
+ try expect.equal(calc.value, 60);
+ }
+};
+
+pub const SUBTRACT = struct {
+ var calc: Calculator = undefined;
+
+ test "tests:before" {
+ calc = Calculator.init();
+ calc.value = 100;
+ }
+
+ test "subtracts positive numbers" {
+ calc.subtract(30);
+ try expect.equal(calc.value, 70);
+ }
+
+ test "subtracts negative numbers" {
+ calc.subtract(-20);
+ try expect.equal(calc.value, 120);
+ }
+
+ test "can go negative" {
+ calc.subtract(150);
+ try expect.equal(calc.value, -50);
+ }
+};
+
+pub const MULTIPLY = struct {
+ var calc: Calculator = undefined;
+
+ test "tests:before" {
+ calc = Calculator.init();
+ calc.value = 10;
+ }
+
+ test "multiplies by positive" {
+ calc.multiply(5);
+ try expect.equal(calc.value, 50);
+ }
+
+ test "multiplies by zero" {
+ calc.multiply(0);
+ try expect.equal(calc.value, 0);
+ }
+
+ test "multiplies by negative" {
+ calc.multiply(-3);
+ try expect.equal(calc.value, -30);
+ }
+};
+
+pub const RESET = struct {
+ var calc: Calculator = undefined;
+
+ test "tests:beforeAll" {
+ std.debug.print(" [RESET] Setting up...\n", .{});
+ }
+
+ test "tests:afterAll" {
+ std.debug.print(" [RESET] Done!\n", .{});
+ }
+
+ test "tests:before" {
+ calc = Calculator.init();
+ calc.value = 999;
+ }
+
+ test "resets to zero" {
+ calc.reset();
+ try expect.equal(calc.value, 0);
+ }
+};
+
+// Example using Let for memoization
+pub const LET_EXAMPLE = struct {
+ fn createExpensiveValue() i32 {
+ // Simulates expensive computation
+ return 42 * 2;
+ }
+
+ const expensive_value = zspec.Let(i32, createExpensiveValue);
+
+ test "tests:after" {
+ expensive_value.reset();
+ }
+
+ test "let memoizes the value" {
+ const first = expensive_value.get();
+ const second = expensive_value.get();
+ try expect.equal(first, 84);
+ try expect.equal(second, 84);
+ }
+};
+
+// Example: Skipping tests with skip_ prefix
+pub const SKIP_EXAMPLE = struct {
+ test "skip_this test is work in progress" {
+ // This test will be skipped and not run
+ try expect.toBeTrue(false); // Would fail if run
+ }
+
+ test "skip_another skipped test" {
+ // This test will also be skipped
+ unreachable;
+ }
+
+};
+
+// Example: Memory Leak Detection
+// The test runner automatically detects memory leaks using std.testing.allocator.
+// Control via environment variables:
+// TEST_DETECT_LEAKS=true (default) - Enable leak detection
+// TEST_FAIL_ON_LEAK=true (default) - Fail tests that leak memory
+//
+// To intentionally create a leak for testing purposes:
+// const leaked = allocator.alloc(u8, 100) catch unreachable;
+// _ = leaked; // Never freed - will trigger leak detection
+pub const MEMORY_LEAK_DETECTION = struct {
+ test "properly cleaned up allocation does not leak" {
+ const data = try allocator.alloc(u8, 100);
+ defer allocator.free(data);
+ @memset(data, 0);
+ try expect.equal(data.len, 100);
+ }
+
+ test "multiple allocations properly freed" {
+ const allocs = try allocator.alloc([*]u8, 5);
+ defer allocator.free(allocs);
+
+ for (allocs, 0..) |_, i| {
+ const block = try allocator.alloc(u8, 64);
+ allocs[i] = block.ptr;
+ }
+
+ // Clean up in reverse order. Note: in Zig 0.16 we have to coerce the
+ // many-pointer slice to `[]u8` explicitly so `Allocator.free` accepts it.
+ var i: usize = allocs.len;
+ while (i > 0) {
+ i -= 1;
+ const slice: []u8 = allocs[i][0..64];
+ allocator.free(slice);
+ }
+
+ try expect.equal(allocs.len, 5);
+ }
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/factory_definitions.zon b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/factory_definitions.zon
new file mode 100644
index 0000000..ae5f6bd
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/factory_definitions.zon
@@ -0,0 +1,32 @@
+.{
+ .user = .{
+ .id = 0,
+ .name = "John Doe",
+ .email = "john@example.com",
+ .age = 25,
+ .active = true,
+ },
+ .admin_user = .{
+ .id = 0,
+ .name = "Admin User",
+ .email = "admin@example.com",
+ .age = 30,
+ .active = true,
+ },
+ .product = .{
+ .id = 0,
+ .name = "Widget",
+ .price = 9.99,
+ .in_stock = true,
+ },
+ .circle_shape = .{
+ .shape = .{ .circle = .{ .radius = 50.0 } },
+ .z_index = 128,
+ .visible = true,
+ },
+ .rectangle_shape = .{
+ .shape = .{ .rectangle = .{ .width = 100.0, .height = 50.0 } },
+ .z_index = 64,
+ .visible = true,
+ },
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/factory_union_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/factory_union_test.zig
new file mode 100644
index 0000000..ba034b0
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/factory_union_test.zig
@@ -0,0 +1,318 @@
+//! Tests for Factory with union type fields
+//! Related to issue #29
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+const Factory = zspec.Factory;
+
+test {
+ zspec.runAll(@This());
+}
+
+const Shape = union(enum) {
+ circle: struct { radius: f32 },
+ rectangle: struct { width: f32, height: f32 },
+};
+
+const ShapeVisual = struct {
+ shape: Shape,
+ z_index: u8,
+};
+
+pub const FACTORY_UNION_EXPLICIT_SYNTAX = struct {
+ test "factory with union field" {
+ const ShapeVisualFactory = Factory.define(ShapeVisual, .{
+ .shape = Shape{ .circle = .{ .radius = 10.0 } },
+ .z_index = 128,
+ });
+
+ const visual = ShapeVisualFactory.build(.{});
+ try expect.equal(visual.z_index, 128);
+
+ switch (visual.shape) {
+ .circle => |c| try expect.equal(c.radius, 10.0),
+ .rectangle => return error.UnexpectedShape,
+ }
+ }
+
+ test "factory with union field override" {
+ const ShapeVisualFactory = Factory.define(ShapeVisual, .{
+ .shape = Shape{ .circle = .{ .radius = 10.0 } },
+ .z_index = 128,
+ });
+
+ // Override with rectangle
+ const visual = ShapeVisualFactory.build(.{
+ .shape = Shape{ .rectangle = .{ .width = 20.0, .height = 30.0 } },
+ });
+
+ switch (visual.shape) {
+ .rectangle => |r| {
+ try expect.equal(r.width, 20.0);
+ try expect.equal(r.height, 30.0);
+ },
+ .circle => return error.UnexpectedShape,
+ }
+ }
+};
+
+pub const FACTORY_UNION_ANONYMOUS_SYNTAX = struct {
+ test "factory with union field using anonymous struct syntax" {
+ // Using anonymous struct syntax for union initialization
+ const ShapeVisualFactory = Factory.define(ShapeVisual, .{
+ .shape = .{ .circle = .{ .radius = 10.0 } },
+ .z_index = 128,
+ });
+
+ const visual = ShapeVisualFactory.build(.{});
+ try expect.equal(visual.z_index, 128);
+
+ switch (visual.shape) {
+ .circle => |c| try expect.equal(c.radius, 10.0),
+ .rectangle => return error.UnexpectedShape,
+ }
+ }
+
+ test "factory with union field override using anonymous struct syntax" {
+ const ShapeVisualFactory = Factory.define(ShapeVisual, .{
+ .shape = .{ .circle = .{ .radius = 10.0 } },
+ .z_index = 128,
+ });
+
+ // Override with rectangle using anonymous struct syntax
+ const visual = ShapeVisualFactory.build(.{
+ .shape = .{ .rectangle = .{ .width = 20.0, .height = 30.0 } },
+ });
+
+ switch (visual.shape) {
+ .rectangle => |r| {
+ try expect.equal(r.width, 20.0);
+ try expect.equal(r.height, 30.0);
+ },
+ .circle => return error.UnexpectedShape,
+ }
+ }
+
+ test "factory trait with union field using anonymous struct syntax" {
+ const ShapeVisualFactory = Factory.define(ShapeVisual, .{
+ .shape = .{ .circle = .{ .radius = 10.0 } },
+ .z_index = 128,
+ });
+
+ // Trait that changes the default shape to rectangle
+ const RectangleVisualFactory = ShapeVisualFactory.trait(.{
+ .shape = .{ .rectangle = .{ .width = 50.0, .height = 25.0 } },
+ });
+
+ const visual = RectangleVisualFactory.build(.{});
+ try expect.equal(visual.z_index, 128);
+
+ switch (visual.shape) {
+ .rectangle => |r| {
+ try expect.equal(r.width, 50.0);
+ try expect.equal(r.height, 25.0);
+ },
+ .circle => return error.UnexpectedShape,
+ }
+ }
+};
+
+// Edge case: void union payload
+pub const FACTORY_UNION_VOID_PAYLOAD = struct {
+ const State = union(enum) {
+ idle: void,
+ running: struct { speed: f32 },
+ stopped: void,
+ };
+
+ const Entity = struct {
+ state: State,
+ id: u32,
+ };
+
+ test "factory with void union payload" {
+ const EntityFactory = Factory.define(Entity, .{
+ .state = .{ .idle = {} },
+ .id = 1,
+ });
+
+ const entity = EntityFactory.build(.{});
+ try expect.equal(entity.id, 1);
+ try expect.toBeTrue(entity.state == .idle);
+ }
+
+ test "factory with void union payload override" {
+ const EntityFactory = Factory.define(Entity, .{
+ .state = .{ .idle = {} },
+ .id = 1,
+ });
+
+ const entity = EntityFactory.build(.{
+ .state = .{ .stopped = {} },
+ });
+ try expect.toBeTrue(entity.state == .stopped);
+ }
+
+ test "factory switching from void to struct payload" {
+ const EntityFactory = Factory.define(Entity, .{
+ .state = .{ .idle = {} },
+ .id = 1,
+ });
+
+ const entity = EntityFactory.build(.{
+ .state = .{ .running = .{ .speed = 5.0 } },
+ });
+
+ switch (entity.state) {
+ .running => |r| try expect.equal(r.speed, 5.0),
+ else => return error.UnexpectedState,
+ }
+ }
+};
+
+// Edge case: optional union fields
+pub const FACTORY_UNION_OPTIONAL = struct {
+ const OptionalShapeEntity = struct {
+ shape: ?Shape,
+ name: []const u8,
+ };
+
+ test "factory with optional union field null" {
+ const EntityFactory = Factory.define(OptionalShapeEntity, .{
+ .shape = null,
+ .name = "empty",
+ });
+
+ const entity = EntityFactory.build(.{});
+ try expect.toBeTrue(entity.shape == null);
+ try std.testing.expectEqualStrings("empty", entity.name);
+ }
+
+ test "factory with optional union field set" {
+ const EntityFactory = Factory.define(OptionalShapeEntity, .{
+ .shape = Shape{ .circle = .{ .radius = 5.0 } },
+ .name = "circle",
+ });
+
+ const entity = EntityFactory.build(.{});
+ try expect.toBeTrue(entity.shape != null);
+
+ if (entity.shape) |shape| {
+ switch (shape) {
+ .circle => |c| try expect.equal(c.radius, 5.0),
+ .rectangle => return error.UnexpectedShape,
+ }
+ }
+ }
+
+ test "factory override optional union from null to value" {
+ const EntityFactory = Factory.define(OptionalShapeEntity, .{
+ .shape = null,
+ .name = "empty",
+ });
+
+ const entity = EntityFactory.build(.{
+ .shape = Shape{ .rectangle = .{ .width = 10.0, .height = 20.0 } },
+ });
+
+ try expect.toBeTrue(entity.shape != null);
+ }
+};
+
+// Edge case: payload struct with default values
+pub const FACTORY_UNION_DEFAULTED_PAYLOAD = struct {
+ const Config = struct {
+ enabled: bool = true,
+ priority: u8 = 10,
+ name: []const u8,
+ };
+
+ const Setting = union(enum) {
+ custom: Config,
+ preset: []const u8,
+ };
+
+ const SettingHolder = struct {
+ setting: Setting,
+ id: u32,
+ };
+
+ test "factory with payload struct omitting defaulted fields" {
+ const SettingFactory = Factory.define(SettingHolder, .{
+ // Only provide required field 'name', rely on defaults for enabled/priority
+ .setting = .{ .custom = .{ .name = "test" } },
+ .id = 1,
+ });
+
+ const holder = SettingFactory.build(.{});
+ try expect.equal(holder.id, 1);
+
+ switch (holder.setting) {
+ .custom => |c| {
+ try std.testing.expectEqualStrings("test", c.name);
+ try expect.toBeTrue(c.enabled); // default value
+ try expect.equal(c.priority, 10); // default value
+ },
+ .preset => return error.UnexpectedSetting,
+ }
+ }
+};
+
+// Edge case: trait chaining with mixed types
+pub const FACTORY_TRAIT_CHAINING = struct {
+ const Item = struct {
+ name: []const u8,
+ value: u32,
+ active: bool,
+ };
+
+ test "trait chaining preserves all fields" {
+ const ItemFactory = Factory.define(Item, .{
+ .name = "default",
+ .value = 0,
+ .active = false,
+ });
+
+ const ActiveFactory = ItemFactory.trait(.{
+ .active = true,
+ });
+
+ const NamedActiveFactory = ActiveFactory.trait(.{
+ .name = "named",
+ });
+
+ const item = NamedActiveFactory.build(.{});
+ try std.testing.expectEqualStrings("named", item.name);
+ try expect.equal(item.value, 0); // from base
+ try expect.toBeTrue(item.active); // from first trait
+ }
+
+ test "trait chaining with union field type changes" {
+ const ShapeVisualFactory = Factory.define(ShapeVisual, .{
+ .shape = .{ .circle = .{ .radius = 10.0 } },
+ .z_index = 128,
+ });
+
+ // First trait: change shape to rectangle
+ const RectFactory = ShapeVisualFactory.trait(.{
+ .shape = .{ .rectangle = .{ .width = 20.0, .height = 30.0 } },
+ });
+
+ // Second trait: change z_index only
+ const HighZRectFactory = RectFactory.trait(.{
+ .z_index = 255,
+ });
+
+ const visual = HighZRectFactory.build(.{});
+ try expect.equal(visual.z_index, 255);
+
+ switch (visual.shape) {
+ .rectangle => |r| {
+ try expect.equal(r.width, 20.0);
+ try expect.equal(r.height, 30.0);
+ },
+ .circle => return error.UnexpectedShape,
+ }
+ }
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/factory_zon_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/factory_zon_test.zig
new file mode 100644
index 0000000..e93257f
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/factory_zon_test.zig
@@ -0,0 +1,426 @@
+//! Tests for Factory.defineFrom() with .zon file loading
+//! Related to issue #31
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+const Factory = zspec.Factory;
+
+test {
+ zspec.runAll(@This());
+}
+
+// Type definitions for testing
+const User = struct {
+ id: u32,
+ name: []const u8,
+ email: []const u8,
+ age: u8,
+ active: bool,
+};
+
+const Product = struct {
+ id: u32,
+ name: []const u8,
+ price: f32,
+ in_stock: bool,
+};
+
+const Shape = union(enum) {
+ circle: struct { radius: f32 },
+ rectangle: struct { width: f32, height: f32 },
+};
+
+const ShapeVisual = struct {
+ shape: Shape,
+ z_index: u8,
+ visible: bool,
+};
+
+// Load factory definitions from .zon file
+const factory_defs = @import("factory_definitions.zon");
+
+// Define factories using defineFrom
+const UserFactory = Factory.defineFrom(User, factory_defs.user);
+const AdminUserFactory = Factory.defineFrom(User, factory_defs.admin_user);
+const ProductFactory = Factory.defineFrom(Product, factory_defs.product);
+const CircleShapeFactory = Factory.defineFrom(ShapeVisual, factory_defs.circle_shape);
+const RectangleShapeFactory = Factory.defineFrom(ShapeVisual, factory_defs.rectangle_shape);
+
+pub const DEFINE_FROM_BASIC = struct {
+ test "defineFrom creates factory with defaults from .zon" {
+ const user = UserFactory.build(.{});
+
+ try std.testing.expectEqualStrings("John Doe", user.name);
+ try std.testing.expectEqualStrings("john@example.com", user.email);
+ try expect.equal(user.age, 25);
+ try expect.toBeTrue(user.active);
+ }
+
+ test "defineFrom allows overriding fields" {
+ const user = UserFactory.build(.{
+ .name = "Jane Doe",
+ .age = 30,
+ });
+
+ try std.testing.expectEqualStrings("Jane Doe", user.name);
+ try std.testing.expectEqualStrings("john@example.com", user.email); // default from .zon
+ try expect.equal(user.age, 30);
+ }
+
+ test "defineFrom with different factory definitions" {
+ const admin = AdminUserFactory.build(.{});
+
+ try std.testing.expectEqualStrings("Admin User", admin.name);
+ try std.testing.expectEqualStrings("admin@example.com", admin.email);
+ try expect.equal(admin.age, 30);
+ }
+
+ test "defineFrom works with product type" {
+ const product = ProductFactory.build(.{});
+
+ try std.testing.expectEqualStrings("Widget", product.name);
+ try expect.equal(product.price, 9.99);
+ try expect.toBeTrue(product.in_stock);
+ }
+
+ test "defineFrom with product override" {
+ const product = ProductFactory.build(.{
+ .name = "Gadget",
+ .price = 19.99,
+ .in_stock = false,
+ });
+
+ try std.testing.expectEqualStrings("Gadget", product.name);
+ try expect.equal(product.price, 19.99);
+ try expect.toBeTrue(!product.in_stock);
+ }
+};
+
+pub const DEFINE_FROM_UNION = struct {
+ test "defineFrom with union type (circle)" {
+ const visual = CircleShapeFactory.build(.{});
+
+ try expect.toBeTrue(visual.visible);
+ try expect.equal(visual.z_index, 128);
+
+ switch (visual.shape) {
+ .circle => |c| try expect.equal(c.radius, 50.0),
+ .rectangle => return error.UnexpectedShape,
+ }
+ }
+
+ test "defineFrom with union type (rectangle)" {
+ const visual = RectangleShapeFactory.build(.{});
+
+ try expect.toBeTrue(visual.visible);
+ try expect.equal(visual.z_index, 64);
+
+ switch (visual.shape) {
+ .rectangle => |r| {
+ try expect.equal(r.width, 100.0);
+ try expect.equal(r.height, 50.0);
+ },
+ .circle => return error.UnexpectedShape,
+ }
+ }
+
+ test "defineFrom with union override" {
+ // Start with circle, override to rectangle
+ const visual = CircleShapeFactory.build(.{
+ .shape = .{ .rectangle = .{ .width = 200.0, .height = 100.0 } },
+ });
+
+ switch (visual.shape) {
+ .rectangle => |r| {
+ try expect.equal(r.width, 200.0);
+ try expect.equal(r.height, 100.0);
+ },
+ .circle => return error.UnexpectedShape,
+ }
+ }
+};
+
+pub const DEFINE_FROM_TRAITS = struct {
+ test "defineFrom factory supports traits" {
+ const InactiveUserFactory = UserFactory.trait(.{
+ .active = false,
+ });
+
+ const user = InactiveUserFactory.build(.{});
+
+ try std.testing.expectEqualStrings("John Doe", user.name); // from .zon
+ try expect.toBeTrue(!user.active); // from trait
+ }
+
+ test "defineFrom factory with chained traits" {
+ const CustomUserFactory = UserFactory.trait(.{
+ .active = false,
+ }).trait(.{
+ .age = 40,
+ });
+
+ const user = CustomUserFactory.build(.{});
+
+ try expect.toBeTrue(!user.active);
+ try expect.equal(user.age, 40);
+ try std.testing.expectEqualStrings("John Doe", user.name); // from .zon
+ }
+};
+
+pub const DEFINE_FROM_EQUIVALENCE = struct {
+ test "defineFrom produces same result as define" {
+ // Factory defined inline
+ const InlineFactory = Factory.define(User, .{
+ .id = 0,
+ .name = "John Doe",
+ .email = "john@example.com",
+ .age = 25,
+ .active = true,
+ });
+
+ const from_inline = InlineFactory.build(.{});
+ const from_zon = UserFactory.build(.{});
+
+ try std.testing.expectEqualStrings(from_inline.name, from_zon.name);
+ try std.testing.expectEqualStrings(from_inline.email, from_zon.email);
+ try expect.equal(from_inline.age, from_zon.age);
+ try expect.equal(from_inline.active, from_zon.active);
+ }
+};
+
+// Types for nested struct tests (issue #33)
+const Color = struct {
+ r: u8,
+ g: u8,
+ b: u8,
+ a: u8,
+};
+
+const SpriteVisual = struct {
+ tint: Color,
+ scale: f32,
+};
+
+// Simulates data from a .zon file (anonymous struct)
+const sprite_zon_data = .{
+ .tint = .{ .r = 255, .g = 128, .b = 64, .a = 255 },
+ .scale = 1.5,
+};
+
+const SpriteVisualFactory = Factory.defineFrom(SpriteVisual, sprite_zon_data);
+
+pub const DEFINE_FROM_NESTED_STRUCT = struct {
+ test "defineFrom with nested struct coerces anonymous struct to named struct" {
+ const sprite = SpriteVisualFactory.build(.{});
+
+ try expect.equal(sprite.tint.r, 255);
+ try expect.equal(sprite.tint.g, 128);
+ try expect.equal(sprite.tint.b, 64);
+ try expect.equal(sprite.tint.a, 255);
+ try expect.equal(sprite.scale, 1.5);
+ }
+
+ test "defineFrom with nested struct allows overrides" {
+ // Override the nested struct with a properly typed Color
+ const sprite = SpriteVisualFactory.build(.{
+ .tint = Color{ .r = 0, .g = 0, .b = 0, .a = 128 },
+ });
+
+ try expect.equal(sprite.tint.r, 0);
+ try expect.equal(sprite.tint.a, 128);
+ }
+
+ test "trait with nested struct from defineFrom" {
+ // Create a trait with a different tint (using anonymous struct)
+ const RedTintFactory = SpriteVisualFactory.trait(.{
+ .tint = .{ .r = 255, .g = 0, .b = 0, .a = 255 },
+ });
+
+ const sprite = RedTintFactory.build(.{});
+
+ try expect.equal(sprite.tint.r, 255);
+ try expect.equal(sprite.tint.g, 0);
+ try expect.equal(sprite.tint.b, 0);
+ }
+
+ test "callsite override with anonymous nested struct" {
+ // Override using anonymous struct syntax at build() callsite
+ const sprite = SpriteVisualFactory.build(.{
+ .tint = .{ .r = 0, .g = 255, .b = 0, .a = 128 },
+ });
+
+ try expect.equal(sprite.tint.r, 0);
+ try expect.equal(sprite.tint.g, 255);
+ try expect.equal(sprite.tint.b, 0);
+ try expect.equal(sprite.tint.a, 128);
+ }
+
+ test "callsite override with anonymous nested struct on trait factory" {
+ const RedTintFactory = SpriteVisualFactory.trait(.{
+ .tint = .{ .r = 255, .g = 0, .b = 0, .a = 255 },
+ });
+
+ // Override the trait's tint with anonymous struct at callsite
+ const sprite = RedTintFactory.build(.{
+ .tint = .{ .r = 0, .g = 0, .b = 255, .a = 64 },
+ });
+
+ try expect.equal(sprite.tint.r, 0);
+ try expect.equal(sprite.tint.g, 0);
+ try expect.equal(sprite.tint.b, 255);
+ try expect.equal(sprite.tint.a, 64);
+ }
+};
+
+// Types for deeply nested struct tests (issue #35)
+const Inner = struct {
+ value: u8,
+ name: []const u8,
+};
+
+const Middle = struct {
+ inner: Inner,
+ count: u32,
+};
+
+const Outer = struct {
+ middle: Middle,
+ label: []const u8,
+};
+
+// Deeply nested .zon data
+const deeply_nested_zon = .{
+ .middle = .{
+ .inner = .{ .value = 42, .name = "nested" },
+ .count = 100,
+ },
+ .label = "outer",
+};
+
+const OuterFactory = Factory.defineFrom(Outer, deeply_nested_zon);
+
+pub const DEFINE_FROM_DEEPLY_NESTED = struct {
+ test "defineFrom with deeply nested structs coerces all levels" {
+ const outer = OuterFactory.build(.{});
+
+ try std.testing.expectEqualStrings("outer", outer.label);
+ try expect.equal(outer.middle.count, 100);
+ try expect.equal(outer.middle.inner.value, 42);
+ try std.testing.expectEqualStrings("nested", outer.middle.inner.name);
+ }
+
+ test "deeply nested struct override at middle level" {
+ const outer = OuterFactory.build(.{
+ .middle = .{
+ .inner = .{ .value = 99, .name = "overridden" },
+ .count = 200,
+ },
+ });
+
+ try expect.equal(outer.middle.count, 200);
+ try expect.equal(outer.middle.inner.value, 99);
+ try std.testing.expectEqualStrings("overridden", outer.middle.inner.name);
+ }
+
+ test "deeply nested struct with trait" {
+ const CustomOuterFactory = OuterFactory.trait(.{
+ .middle = .{
+ .inner = .{ .value = 77, .name = "from trait" },
+ .count = 50,
+ },
+ });
+
+ const outer = CustomOuterFactory.build(.{});
+
+ try expect.equal(outer.middle.inner.value, 77);
+ try std.testing.expectEqualStrings("from trait", outer.middle.inner.name);
+ try expect.equal(outer.middle.count, 50);
+ }
+};
+
+// Types for union with nested struct validation (issue #36)
+const Position = struct {
+ x: f32,
+ y: f32,
+};
+
+const CircleData = struct {
+ center: Position,
+ radius: f32,
+};
+
+const RectData = struct {
+ origin: Position,
+ width: f32,
+ height: f32,
+};
+
+const ComplexShape = union(enum) {
+ circle: CircleData,
+ rect: RectData,
+};
+
+const Canvas = struct {
+ shape: ComplexShape,
+ name: []const u8,
+};
+
+const canvas_circle_zon = .{
+ .shape = .{ .circle = .{ .center = .{ .x = 10.0, .y = 20.0 }, .radius = 5.0 } },
+ .name = "my circle",
+};
+
+const canvas_rect_zon = .{
+ .shape = .{ .rect = .{ .origin = .{ .x = 0.0, .y = 0.0 }, .width = 100.0, .height = 50.0 } },
+ .name = "my rect",
+};
+
+const CircleCanvasFactory = Factory.defineFrom(Canvas, canvas_circle_zon);
+const RectCanvasFactory = Factory.defineFrom(Canvas, canvas_rect_zon);
+
+pub const DEFINE_FROM_UNION_WITH_NESTED = struct {
+ test "union with deeply nested struct (circle)" {
+ const canvas = CircleCanvasFactory.build(.{});
+
+ try std.testing.expectEqualStrings("my circle", canvas.name);
+ switch (canvas.shape) {
+ .circle => |c| {
+ try expect.equal(c.center.x, 10.0);
+ try expect.equal(c.center.y, 20.0);
+ try expect.equal(c.radius, 5.0);
+ },
+ .rect => return error.UnexpectedShape,
+ }
+ }
+
+ test "union with deeply nested struct (rect)" {
+ const canvas = RectCanvasFactory.build(.{});
+
+ try std.testing.expectEqualStrings("my rect", canvas.name);
+ switch (canvas.shape) {
+ .rect => |r| {
+ try expect.equal(r.origin.x, 0.0);
+ try expect.equal(r.origin.y, 0.0);
+ try expect.equal(r.width, 100.0);
+ try expect.equal(r.height, 50.0);
+ },
+ .circle => return error.UnexpectedShape,
+ }
+ }
+
+ test "override union with deeply nested anonymous struct" {
+ const canvas = CircleCanvasFactory.build(.{
+ .shape = .{ .rect = .{ .origin = .{ .x = 5.0, .y = 5.0 }, .width = 200.0, .height = 100.0 } },
+ });
+
+ switch (canvas.shape) {
+ .rect => |r| {
+ try expect.equal(r.origin.x, 5.0);
+ try expect.equal(r.origin.y, 5.0);
+ try expect.equal(r.width, 200.0);
+ },
+ .circle => return error.UnexpectedShape,
+ }
+ }
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/fixture_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/fixture_test.zig
new file mode 100644
index 0000000..4feb68b
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/fixture_test.zig
@@ -0,0 +1,378 @@
+//! Tests for Fixture module
+//! Related to RFC 001 (issue #38)
+
+const std = @import("std");
+const zspec = @import("zspec");
+const expect = zspec.expect;
+const Fixture = zspec.Fixture;
+
+test {
+ zspec.runAll(@This());
+}
+
+// =============================================================================
+// Type Definitions
+// =============================================================================
+
+const User = struct {
+ id: u32,
+ name: []const u8,
+ email: []const u8,
+ active: bool = true,
+};
+
+const Product = struct {
+ id: u32,
+ name: []const u8,
+ price: f32,
+ seller_id: u32,
+};
+
+const Order = struct {
+ id: u32,
+ user_id: u32,
+ product_id: u32,
+ quantity: u32,
+};
+
+const Position = struct { x: f32, y: f32 };
+const Health = struct { current: u32, max: u32 };
+
+const EnemyKind = enum { slime, goblin, dragon };
+
+const PlayerData = struct {
+ pos: Position,
+ health: Health,
+};
+
+const EnemyData = struct {
+ pos: Position,
+ health: Health,
+ kind: EnemyKind,
+};
+
+const CheckoutScenario = struct {
+ user: User,
+ product: Product,
+ order: Order,
+};
+
+const BattleScenario = struct {
+ player: PlayerData,
+ enemies: [3]EnemyData,
+};
+
+const Shape = union(enum) {
+ circle: struct { radius: f32 },
+ rectangle: struct { width: f32, height: f32 },
+};
+
+const ShapeVisual = struct {
+ shape: Shape,
+ z_index: u8,
+ visible: bool,
+};
+
+const Color = struct { r: u8, g: u8, b: u8, a: u8 };
+const SpriteVisual = struct { tint: Color, scale: f32 };
+
+const Inner = struct { value: u8, name: []const u8 };
+const Middle = struct { inner: Inner, count: u32 };
+const Outer = struct { middle: Middle, label: []const u8 };
+
+// =============================================================================
+// Fixture Definitions
+// =============================================================================
+
+// Single-struct fixture from .zon file
+const UserFixture = Fixture.define(User, @import("fixtures/user.zon"));
+
+// Scenario fixture from .zon file
+const CheckoutFixture = Fixture.define(CheckoutScenario, @import("fixtures/checkout.zon"));
+
+// Nested structs + arrays from .zon file
+const BattleFixture = Fixture.define(BattleScenario, @import("fixtures/battle.zon"));
+
+// Inline fixture definitions (for testing without .zon files)
+const InlineUserFixture = Fixture.define(User, .{
+ .id = 1,
+ .name = "John Doe",
+ .email = "john@example.com",
+ .active = true,
+});
+
+const ShapeFixture = Fixture.define(ShapeVisual, .{
+ .shape = .{ .circle = .{ .radius = 25.0 } },
+ .z_index = 10,
+ .visible = true,
+});
+
+const SpriteFixture = Fixture.define(SpriteVisual, .{
+ .tint = .{ .r = 255, .g = 128, .b = 64, .a = 255 },
+ .scale = 1.5,
+});
+
+const NestedFixture = Fixture.define(Outer, .{
+ .middle = .{
+ .inner = .{ .value = 42, .name = "nested" },
+ .count = 100,
+ },
+ .label = "outer",
+});
+
+// =============================================================================
+// Tests
+// =============================================================================
+
+pub const SINGLE_STRUCT = struct {
+ test "create with defaults" {
+ const user = InlineUserFixture.create(.{});
+
+ try std.testing.expectEqualStrings("John Doe", user.name);
+ try std.testing.expectEqualStrings("john@example.com", user.email);
+ try expect.equal(user.id, 1);
+ try expect.toBeTrue(user.active);
+ }
+
+ test "create with overrides" {
+ const user = InlineUserFixture.create(.{
+ .name = "Jane Smith",
+ .email = "jane@example.com",
+ });
+
+ try std.testing.expectEqualStrings("Jane Smith", user.name);
+ try std.testing.expectEqualStrings("jane@example.com", user.email);
+ // Defaults preserved
+ try expect.equal(user.id, 1);
+ try expect.toBeTrue(user.active);
+ }
+
+ test "create with all fields overridden" {
+ const user = InlineUserFixture.create(.{
+ .id = 99,
+ .name = "Custom",
+ .email = "custom@test.com",
+ .active = false,
+ });
+
+ try expect.equal(user.id, 99);
+ try std.testing.expectEqualStrings("Custom", user.name);
+ try expect.toBeTrue(!user.active);
+ }
+};
+
+pub const SCENARIO = struct {
+ test "create multi-struct scenario" {
+ const s = CheckoutFixture.create(.{});
+
+ try expect.equal(s.user.id, 1);
+ try std.testing.expectEqualStrings("John Doe", s.user.name);
+ try expect.equal(s.product.id, 10);
+ try std.testing.expectEqualStrings("Widget", s.product.name);
+ try expect.equal(s.order.id, 100);
+ try expect.equal(s.order.quantity, 2);
+ }
+
+ test "cross-references are consistent" {
+ const s = CheckoutFixture.create(.{});
+
+ try expect.equal(s.order.user_id, s.user.id);
+ try expect.equal(s.order.product_id, s.product.id);
+ try expect.equal(s.product.seller_id, s.user.id);
+ }
+
+ test "scenario with overrides" {
+ const s = CheckoutFixture.create(.{
+ .order = .{ .id = 200, .user_id = 1, .product_id = 10, .quantity = 5 },
+ });
+
+ try expect.equal(s.order.id, 200);
+ try expect.equal(s.order.quantity, 5);
+ // Other structs unchanged
+ try std.testing.expectEqualStrings("John Doe", s.user.name);
+ }
+
+ test "partial nested override in scenario" {
+ // Override only user name — other user fields and other structs preserved
+ const s = CheckoutFixture.create(.{
+ .user = .{ .name = "Jane" },
+ });
+
+ try std.testing.expectEqualStrings("Jane", s.user.name);
+ // Other user fields from .zon
+ try expect.equal(s.user.id, 1);
+ try std.testing.expectEqualStrings("john@example.com", s.user.email);
+ // Other structs unchanged
+ try expect.equal(s.product.id, 10);
+ try expect.equal(s.order.quantity, 2);
+ }
+};
+
+pub const ARRAYS = struct {
+ test "fixed-size array from .zon tuple" {
+ const battle = BattleFixture.create(.{});
+
+ try expect.equal(battle.enemies[0].kind, .slime);
+ try expect.equal(battle.enemies[0].health.current, 20);
+ try expect.equal(battle.enemies[0].pos.x, 50.0);
+
+ try expect.equal(battle.enemies[1].kind, .goblin);
+ try expect.equal(battle.enemies[1].health.current, 50);
+
+ try expect.equal(battle.enemies[2].kind, .dragon);
+ try expect.equal(battle.enemies[2].health.current, 200);
+ }
+
+ test "array elements have correct nested struct coercion" {
+ const battle = BattleFixture.create(.{});
+
+ // Verify deeply nested values in array elements
+ try expect.equal(battle.enemies[0].pos.y, 30.0);
+ try expect.equal(battle.enemies[1].pos.x, 80.0);
+ try expect.equal(battle.enemies[2].health.max, 200);
+ }
+
+ test "inline array fixture" {
+ const Item = struct { id: u32, name: []const u8 };
+ const Inventory = struct {
+ owner: []const u8,
+ items: [2]Item,
+ };
+
+ const InvFixture = Fixture.define(Inventory, .{
+ .owner = "Alice",
+ .items = .{
+ .{ .id = 1, .name = "Sword" },
+ .{ .id = 2, .name = "Shield" },
+ },
+ });
+
+ const inv = InvFixture.create(.{});
+ try std.testing.expectEqualStrings("Alice", inv.owner);
+ try expect.equal(inv.items[0].id, 1);
+ try std.testing.expectEqualStrings("Sword", inv.items[0].name);
+ try expect.equal(inv.items[1].id, 2);
+ try std.testing.expectEqualStrings("Shield", inv.items[1].name);
+ }
+};
+
+pub const NESTED_STRUCTS = struct {
+ test "deeply nested struct coercion" {
+ const outer = NestedFixture.create(.{});
+
+ try std.testing.expectEqualStrings("outer", outer.label);
+ try expect.equal(outer.middle.count, 100);
+ try expect.equal(outer.middle.inner.value, 42);
+ try std.testing.expectEqualStrings("nested", outer.middle.inner.name);
+ }
+
+ test "override nested struct fields" {
+ const outer = NestedFixture.create(.{
+ .middle = .{
+ .inner = .{ .value = 99, .name = "overridden" },
+ .count = 200,
+ },
+ });
+
+ try expect.equal(outer.middle.count, 200);
+ try expect.equal(outer.middle.inner.value, 99);
+ try std.testing.expectEqualStrings("overridden", outer.middle.inner.name);
+ }
+
+ test "sprite nested struct coercion" {
+ const sprite = SpriteFixture.create(.{});
+
+ try expect.equal(sprite.tint.r, 255);
+ try expect.equal(sprite.tint.g, 128);
+ try expect.equal(sprite.tint.b, 64);
+ try expect.equal(sprite.tint.a, 255);
+ try expect.equal(sprite.scale, 1.5);
+ }
+
+ test "override nested struct at callsite" {
+ const sprite = SpriteFixture.create(.{
+ .tint = .{ .r = 0, .g = 255, .b = 0, .a = 128 },
+ });
+
+ try expect.equal(sprite.tint.r, 0);
+ try expect.equal(sprite.tint.g, 255);
+ try expect.equal(sprite.tint.b, 0);
+ try expect.equal(sprite.tint.a, 128);
+ }
+
+ test "partial nested override preserves defaults" {
+ // Override only one field in nested struct — others preserved from .zon
+ const sprite = SpriteFixture.create(.{
+ .tint = .{ .r = 0 },
+ });
+
+ try expect.equal(sprite.tint.r, 0); // overridden
+ try expect.equal(sprite.tint.g, 128); // from .zon default
+ try expect.equal(sprite.tint.b, 64); // from .zon default
+ try expect.equal(sprite.tint.a, 255); // from .zon default
+ try expect.equal(sprite.scale, 1.5); // from .zon default
+ }
+};
+
+pub const UNIONS = struct {
+ test "union field from anonymous struct" {
+ const visual = ShapeFixture.create(.{});
+
+ try expect.toBeTrue(visual.visible);
+ try expect.equal(visual.z_index, 10);
+
+ switch (visual.shape) {
+ .circle => |c| try expect.equal(c.radius, 25.0),
+ .rectangle => return error.UnexpectedShape,
+ }
+ }
+
+ test "override union field" {
+ const visual = ShapeFixture.create(.{
+ .shape = .{ .rectangle = .{ .width = 100.0, .height = 50.0 } },
+ });
+
+ switch (visual.shape) {
+ .rectangle => |r| {
+ try expect.equal(r.width, 100.0);
+ try expect.equal(r.height, 50.0);
+ },
+ .circle => return error.UnexpectedShape,
+ }
+ }
+};
+
+pub const ZON_FILE_LOADING = struct {
+ test "load single struct from .zon file" {
+ const user = UserFixture.create(.{});
+
+ try expect.equal(user.id, 1);
+ try std.testing.expectEqualStrings("John Doe", user.name);
+ try std.testing.expectEqualStrings("john@example.com", user.email);
+ try expect.toBeTrue(user.active);
+ }
+
+ test "load single struct with override" {
+ const user = UserFixture.create(.{ .name = "Jane" });
+
+ try std.testing.expectEqualStrings("Jane", user.name);
+ // Defaults from .zon
+ try std.testing.expectEqualStrings("john@example.com", user.email);
+ }
+
+ test "load scenario from .zon file" {
+ const s = CheckoutFixture.create(.{});
+
+ try expect.equal(s.order.user_id, s.user.id);
+ try expect.equal(s.order.product_id, s.product.id);
+ }
+
+ test "load battle scenario from .zon file" {
+ const battle = BattleFixture.create(.{});
+
+ try expect.equal(battle.player.pos.x, 0.0);
+ try expect.equal(battle.player.health.current, 100);
+ try expect.equal(battle.enemies[0].kind, .slime);
+ try expect.equal(battle.enemies[2].kind, .dragon);
+ }
+};
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/fixtures/battle.zon b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/fixtures/battle.zon
new file mode 100644
index 0000000..83163ec
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/fixtures/battle.zon
@@ -0,0 +1,23 @@
+.{
+ .player = .{
+ .pos = .{ .x = 0.0, .y = 0.0 },
+ .health = .{ .current = 100, .max = 100 },
+ },
+ .enemies = .{
+ .{
+ .pos = .{ .x = 50.0, .y = 30.0 },
+ .health = .{ .current = 20, .max = 20 },
+ .kind = .slime,
+ },
+ .{
+ .pos = .{ .x = 80.0, .y = 60.0 },
+ .health = .{ .current = 50, .max = 50 },
+ .kind = .goblin,
+ },
+ .{
+ .pos = .{ .x = 120.0, .y = 10.0 },
+ .health = .{ .current = 200, .max = 200 },
+ .kind = .dragon,
+ },
+ },
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/fixtures/checkout.zon b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/fixtures/checkout.zon
new file mode 100644
index 0000000..3f5ebe2
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/fixtures/checkout.zon
@@ -0,0 +1,5 @@
+.{
+ .user = .{ .id = 1, .name = "John Doe", .email = "john@example.com" },
+ .product = .{ .id = 10, .name = "Widget", .price = 29.99, .seller_id = 1 },
+ .order = .{ .id = 100, .user_id = 1, .product_id = 10, .quantity = 2 },
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/fixtures/user.zon b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/fixtures/user.zon
new file mode 100644
index 0000000..e1310b5
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/fixtures/user.zon
@@ -0,0 +1,6 @@
+.{
+ .id = 1,
+ .name = "John Doe",
+ .email = "john@example.com",
+ .active = true,
+}
diff --git a/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/testing_io_test.zig b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/testing_io_test.zig
new file mode 100644
index 0000000..f553dc3
--- /dev/null
+++ b/zig-pkg/zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw/tests/testing_io_test.zig
@@ -0,0 +1,77 @@
+//! Regression test for issue #44.
+//!
+//! When zspec's `src/runner.zig` is used as the `.simple` mode
+//! test_runner, every test using `std.testing.io` (which captures
+//! `&std.testing.io_instance` at comptime — see testing.zig:34-35
+//! and Io/Threaded.zig:1806) would route through a zero-initialized
+//! `Io.Threaded` and deadlock on linux for any op that needs the
+//! worker pool (tmpDir + writeFile, realPathFileAlloc, file-not-
+//! found readFileAlloc, …). macOS happened to satisfy more zero-init
+//! paths so the bug only surfaced on linux CI.
+//!
+//! The fix is a single-shot `testing.io_instance = .init(...)` at
+//! the top of `runner.main()`. This file ensures that fix stays in
+//! place: removing it puts CI back into the 6-hour-timeout regime.
+
+const std = @import("std");
+const zspec = @import("zspec");
+
+test {
+ zspec.runAll(@This());
+}
+
+pub const TestingIoTests = struct {
+ test "tmpDir + writeFile + readFileAlloc round-trips via std.testing.io" {
+ // The minimal sequence that deadlocked before the fix.
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+
+ // `tmpDir` plants the dir at `.zig-cache/tmp//`;
+ // build a cwd-relative path rather than going through
+ // `realPathFileAlloc(io, ".")` (which has its own linux
+ // deadlock signature in this same context — see #44).
+ const path = try std.fs.path.join(
+ std.testing.allocator,
+ &.{ ".zig-cache", "tmp", &tmp.sub_path, "regression.txt" },
+ );
+ defer std.testing.allocator.free(path);
+
+ const payload = "issue-44";
+ try std.Io.Dir.cwd().writeFile(std.testing.io, .{
+ .sub_path = path,
+ .data = payload,
+ });
+
+ const read_back = try std.Io.Dir.cwd().readFileAlloc(
+ std.testing.io,
+ path,
+ std.testing.allocator,
+ .limited(1 << 14),
+ );
+ defer std.testing.allocator.free(read_back);
+
+ try std.testing.expectEqualStrings(payload, read_back);
+ }
+
+ test "readFileAlloc on a missing path returns FileNotFound, doesn't hang" {
+ // The other axis of the linux deadlock — readFileAlloc on a
+ // path that doesn't exist. Before the fix this never returned;
+ // after the fix it returns the expected error promptly.
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+
+ const path = try std.fs.path.join(
+ std.testing.allocator,
+ &.{ ".zig-cache", "tmp", &tmp.sub_path, "does-not-exist.txt" },
+ );
+ defer std.testing.allocator.free(path);
+
+ const result = std.Io.Dir.cwd().readFileAlloc(
+ std.testing.io,
+ path,
+ std.testing.allocator,
+ .limited(1 << 14),
+ );
+ try std.testing.expectError(error.FileNotFound, result);
+ }
+};