Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = .zig_utils,
.version = "0.7.0",
.version = "0.7.1",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The zig-pkg/ directory containing the source code of the zspec dependency seems to have been accidentally committed to the repository. Since zspec is already declared as a package dependency in build.zig.zon and is automatically fetched by the Zig package manager, committing its source code under zig-pkg/ is redundant and bloats the repository.

Please remove the zig-pkg/ directory from the repository and add it to your .gitignore file to prevent it from being committed in the future.

.fingerprint = 0x6dc482caf73c4a75,
.minimum_zig_version = "0.16.0",

Expand Down
16 changes: 16 additions & 0 deletions src/floyd_warshall_optimized.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines +160 to 176

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The cycle guard condition steps > max_steps (where max_steps = self.size) allows the path to grow larger than the total number of nodes in the graph before bailing out. Since a valid simple shortest path can visit each node at most once, it can have at most self.size - 1 edges (and thus at most self.size - 1 iterations of the loop).

Checking steps >= self.size is a tighter and more correct bound that prevents appending redundant nodes to the path list and avoids the need for the intermediate max_steps constant.

            var steps: usize = 0;
            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 >= self.size) 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);
}
Expand All @@ -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;
}
Comment on lines +184 to 196

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similarly to setPathWithMapping, the cycle guard condition can be tightened to steps >= self.size to prevent unnecessary appends and avoid the intermediate max_steps constant.

            var steps: usize = 0;
            while (current != v_node) {
                // See `setPathWithMapping` — guards an inconsistent `next`
                // matrix cycle from looping forever (hang + unbounded alloc).
                if (steps >= self.size) 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);
}
Expand Down
32 changes: 32 additions & 0 deletions tests/floyd_warshall_optimized_test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
};
};
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.{
.name = .zspec,
.version = "0.9.1",
.fingerprint = 0x940ac7516d8ba28d,
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"tests",
"examples",
},
.dependencies = .{},
}
Original file line number Diff line number Diff line change
@@ -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");
}

Loading
Loading