diff --git a/build.zig.zon b/build.zig.zon index ee60c15..eba6fa0 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .zig_utils, - .version = "0.7.0", + .version = "0.7.1", .fingerprint = 0x6dc482caf73c4a75, .minimum_zig_version = "0.16.0", diff --git a/src/floyd_warshall_optimized.zig b/src/floyd_warshall_optimized.zig index 8917169..80d8dc9 100644 --- a/src/floyd_warshall_optimized.zig +++ b/src/floyd_warshall_optimized.zig @@ -157,12 +157,22 @@ pub fn FloydWarshallOptimized(comptime config: Config) type { /// Returns error.NoPathFound if no path exists between the nodes pub fn setPathWithMapping(self: *const Self, path_list: *std.array_list.Managed(u32), u_node: u32, v_node: u32) PathError!void { var current = u_node; + var steps: usize = 0; + const max_steps: usize = self.size; // a shortest path visits each node at most once while (current != v_node) { + // Cycle / corrupt next-hop guard: without this, an inconsistent + // `next` matrix (a cycle that never reaches `v_node`) loops + // forever, growing `path_list` unbounded — a hard hang + OOM. + // A real shortest path is at most `size` hops, so anything + // longer is a cycle. Surfaced as a permanent freeze + memory + // blow-up loading a 1000-worker flying-platform colony. + if (steps > max_steps) return error.NoPathFound; try path_list.append(current); current = self.nextWithMapping(current, v_node); if (current == INF) { return error.NoPathFound; } + steps += 1; } try path_list.append(v_node); } @@ -171,12 +181,18 @@ pub fn FloydWarshallOptimized(comptime config: Config) type { /// Returns error.NoPathFound if no path exists between the nodes pub fn setPathWithMappingUnmanaged(self: *const Self, allocator: std.mem.Allocator, path_list: *std.ArrayListUnmanaged(u32), u_node: u32, v_node: u32) PathError!void { var current = u_node; + var steps: usize = 0; + const max_steps: usize = self.size; // a shortest path visits each node at most once while (current != v_node) { + // See `setPathWithMapping` — guards an inconsistent `next` + // matrix cycle from looping forever (hang + unbounded alloc). + if (steps > max_steps) return error.NoPathFound; try path_list.append(allocator, current); current = self.nextWithMapping(current, v_node); if (current == INF) { return error.NoPathFound; } + steps += 1; } try path_list.append(allocator, v_node); } diff --git a/tests/floyd_warshall_optimized_test.zig b/tests/floyd_warshall_optimized_test.zig index ca9c887..01c3570 100644 --- a/tests/floyd_warshall_optimized_test.zig +++ b/tests/floyd_warshall_optimized_test.zig @@ -126,5 +126,37 @@ pub const FloydWarshallOptimizedSpec = struct { try expect.equal(path.items[2], 30); try expect.equal(path.items[3], 40); } + + test "bails on a cyclic next-hop instead of hanging" { + const allocator = std.testing.allocator; + + var fw = FloydWarshallSimd.init(allocator); + defer fw.deinit(); + + fw.resize(4); + try fw.clean(); + try fw.addEdgeWithMapping(10, 20, 1); + try fw.addEdgeWithMapping(20, 30, 1); + try fw.addEdgeWithMapping(30, 40, 1); + fw.generate(); + + // Corrupt the next-hop matrix into a 10<->20 cycle toward goal 40: + // following the chain never reaches 40. Without the length cap this + // loops forever, growing `path` unbounded (the 1000-worker + // flying-platform load hang + OOM). Insertion-order indices: + // 10->0, 20->1, 30->2, 40->3; matrix is row-major `size`-wide. + const size: u32 = 4; + const idx_a: u32 = 0; // node 10 + const idx_b: u32 = 1; // node 20 + const idx_d: u32 = 3; // node 40 + fw.next[idx_a * size + idx_d] = idx_b; // from 10 toward 40 -> 20 + fw.next[idx_b * size + idx_d] = idx_a; // from 20 toward 40 -> back to 10 + + var path = std.ArrayListUnmanaged(u32){}; + defer path.deinit(allocator); + + // Must TERMINATE with an error, not hang. + try std.testing.expectError(error.NoPathFound, fw.setPathWithMappingUnmanaged(allocator, &path, 10, 40)); + } }; }; diff --git a/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/build.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/build.zig new file mode 100644 index 0000000..8bfd5cb --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/build.zig @@ -0,0 +1,181 @@ +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); + + 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); + + 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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/build.zig.zon b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/build.zig.zon new file mode 100644 index 0000000..8e453f2 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/basic_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/basic_test.zig new file mode 100644 index 0000000..73b4faa --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/ecs_integration_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/ecs_integration_test.zig new file mode 100644 index 0000000..2dbc624 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/factory_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/factory_test.zig new file mode 100644 index 0000000..b719667 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/factory_zon_example.zon b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/factory_zon_example.zon new file mode 100644 index 0000000..d05da08 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/factory_zon_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/factory_zon_test.zig new file mode 100644 index 0000000..05b520e --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/fixture_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/fixture_test.zig new file mode 100644 index 0000000..09faba9 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/fixtures.zon b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/fixtures.zon new file mode 100644 index 0000000..161e193 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/fsm_integration_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/fsm_integration_test.zig new file mode 100644 index 0000000..715d944 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/hooks_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/hooks_test.zig new file mode 100644 index 0000000..4f10604 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/let_memoization_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/let_memoization_test.zig new file mode 100644 index 0000000..f01ab15 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/matchers_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/matchers_test.zig new file mode 100644 index 0000000..228e1bd --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/nested_contexts_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/examples/nested_contexts_test.zig new file mode 100644 index 0000000..5a1c666 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/coerce.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/coerce.zig new file mode 100644 index 0000000..aa90da2 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/factory.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/factory.zig new file mode 100644 index 0000000..acb434b --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/fixture.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/fixture.zig new file mode 100644 index 0000000..4a86a18 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/integrations/ecs.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/integrations/ecs.zig new file mode 100644 index 0000000..12f052c --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/integrations/fsm.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/integrations/fsm.zig new file mode 100644 index 0000000..d94274e --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/junit.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/junit.zig new file mode 100644 index 0000000..ef6f3d6 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/matchers.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/matchers.zig new file mode 100644 index 0000000..0393ad4 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/runner.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/runner.zig new file mode 100644 index 0000000..1377b4e --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/runner.zig @@ -0,0 +1,775 @@ +//! 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; + + 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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/zspec.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/src/zspec.zig new file mode 100644 index 0000000..f1b27d4 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/example_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/example_test.zig new file mode 100644 index 0000000..688a449 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/factory_definitions.zon b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/factory_definitions.zon new file mode 100644 index 0000000..ae5f6bd --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/factory_union_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/factory_union_test.zig new file mode 100644 index 0000000..ba034b0 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/factory_zon_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/factory_zon_test.zig new file mode 100644 index 0000000..e93257f --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/fixture_test.zig b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/fixture_test.zig new file mode 100644 index 0000000..4feb68b --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/fixtures/battle.zon b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/fixtures/battle.zon new file mode 100644 index 0000000..83163ec --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/fixtures/checkout.zon b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/fixtures/checkout.zon new file mode 100644 index 0000000..3f5ebe2 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/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-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/fixtures/user.zon b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/fixtures/user.zon new file mode 100644 index 0000000..e1310b5 --- /dev/null +++ b/zig-pkg/zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K/tests/fixtures/user.zon @@ -0,0 +1,6 @@ +.{ + .id = 1, + .name = "John Doe", + .email = "john@example.com", + .active = true, +}