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
7 changes: 7 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ pub fn build(b: *std.Build) void {
gui_tests_exe.root_module.linkLibrary(zgui_te.artifact("imgui"));
gui_tests_exe.root_module.addImport("zstbi", zstbi.module("root"));
gui_tests_exe.root_module.addImport("flow_codegen", flow_codegen_module);
// App.renderFrame transitively imports nfd via the file-dialog
// code path; without this addImport the test binary fails to
// compile as soon as the previously-dead `Callbacks.gui`/`run`
// bodies are analyzed (this addition originally landed in #106
// and was inadvertently dropped during #115's review-fix
// cherry-pick rebase).
gui_tests_exe.root_module.addImport("nfd", nfd.module("nfd"));
gui_tests_exe.root_module.addImport("engine", engine_module);

const run_gui_tests = b.addRunArtifact(gui_tests_exe);
Expand Down
11 changes: 9 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,15 @@
.hash = "nfd-0.1.0-y82kbsSLBgCxj9gDvxK7fxPFYnI3oe2wWx6M9ykp9Gio",
},
.zspec = .{
.url = "https://github.com/apotema/zspec/archive/v0.9.1.tar.gz",
.hash = "zspec-0.9.1-jaKLbbX4AwBKANdetxzzWc3UTO0UY0lcJJzTagQHlt5K",
.url = "https://github.com/apotema/zspec/archive/v0.9.2.tar.gz",
// Hash prefix says `0.9.1` because upstream's own
// `build.zig.zon` still declares `.version = "0.9.1"` at
// the v0.9.2 tag — Zig's package manager bakes that
// string into the hash and rejects manual edits with a
// "hash mismatch" error. The base64 portion is the only
// integrity-relevant part; the prefix is cosmetic
// metadata sourced from upstream.
.hash = "zspec-0.9.1-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw",

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 hash prefix for zspec still references version 0.9.1 while the URL has been updated to v0.9.2. While Zig's package manager primarily uses the multihash suffix for integrity, having a version mismatch in the hash string is inconsistent and can be confusing for maintenance. It is recommended to update the prefix to match the new version.

            .hash = "zspec-0.9.2-jaKLbXgMBACFwbNjflhbMyP113leoYjboxn-1UOP-FGw",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reply: tried this — Zig 0.16's package manager rejects the edit with hash mismatch: manifest declares zspec-0.9.2-... but the fetched package has zspec-0.9.1-.... The version prefix is sourced from upstream's own build.zig.zon .version field (still "0.9.1" at the v0.9.2 tag); only the base64 suffix is integrity-relevant. Left the prefix as-is with a comment in build.zig.zon explaining. The real fix is an upstream version bump — filed mentally for next zspec patch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reply: Zig 0.16's package manager rejects this edit — hash mismatch: manifest declares zspec-0.9.2-... but the fetched package has zspec-0.9.1-.... The prefix is baked from upstream's build.zig.zon .version field, which still reads "0.9.1" at the v0.9.2 tag. Only the base64 portion is integrity-checked; the prefix is cosmetic metadata sourced from upstream. Documented in a comment on the field — see df4b631.

},
.zstbi = .{
.url = "git+https://github.com/zig-gamedev/zstbi?ref=main#3813f5f113b26f644cfa01a30155cc76b3526e91",
Expand Down
44 changes: 44 additions & 0 deletions src/test_fixtures.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! zspec Factory definitions shared across tests.
//!
//! Pattern recap (informs #109/#120 follow-up work):
//!
//! * `Factory.defineFrom(T, @import("…zon"))` — concise, fixture in
//! a separate `.zon` file, field-name typo detection at comptime.
//! Works only when every field of T has a concrete value the zon
//! literal can coerce to. Slice fields (`[]const X = &.{}`) hit the
//! `@as(FieldType, .{})` coercion path and fail to compile, so
//! reserve `defineFrom` for leaf types like `ResourceDef`.
//!
//! * `Factory.define(T, .{ ...defaults })` — same comptime defaults,
//! authored in Zig source. Necessary when T has slice or pointer
//! fields because we can write `&.{}` and `@as([]const X, &.{})`
//! explicitly. Use for outer types like `ProjectConfig`.
//!
//! Strings inside the defaults are static (comptime). `.build({})`
//! returns a value whose strings outlive any test arena, so the
//! resulting `ResourceDef` / `ProjectConfig` can be assigned directly
//! to a project arena-owned slice without per-field `dupe()` ceremony.
const zspec = @import("zspec");
const Factory = zspec.Factory;
const project = @import("project.zig");

const resource_zon = @import("test_fixtures/resource.zon");

pub const ResourceFactory = Factory.defineFrom(project.ResourceDef, resource_zon);

pub const ProjectConfigFactory = Factory.define(project.ProjectConfig, .{
.name = "factory_project",
.description = "",
.title = "Factory Project",
.width = 1280,
.height = 720,
.target_fps = 60,
.backend = .raylib,
.ecs = .zig_ecs,
.initial_scene = "main",
.core_version = "1.12.0",
.engine_version = "1.35.0",
.gfx_version = "1.10.0",
.assembler_version = "0.17.0",
.resources = @as([]const project.ResourceDef, &.{}),
});
5 changes: 5 additions & 0 deletions src/test_fixtures/resource.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.{
.name = "sprites",
.json = "assets/sprites.json",
.texture = "assets/sprites.png",
}
67 changes: 59 additions & 8 deletions src/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const flow_types = @import("flows/types.zig");
const prefs = @import("prefs.zig");
const io_global = @import("io_global.zig");
const game_view = @import("game_view.zig");
const test_fixtures = @import("test_fixtures.zig");

/// Wall-clock seconds since the Unix epoch; replacement for the
/// `std.time.timestamp` helper removed in Zig 0.16. Used only to
Expand Down Expand Up @@ -1886,21 +1887,19 @@ pub const ProjectFileTests = struct {
const temp_dir = try createTempDir(allocator);
defer deleteTempDir(allocator, temp_dir);

// Build a project with one resource directly via the arena so
// the slice is owned correctly. Save then reload via a fresh
// ProjectManager and assert the resource came back intact.
// Save a project carrying one Factory-built ResourceDef, reload
// via a fresh ProjectManager, assert the resource came back
// intact. Factory.defineFrom validates each field name against
// ResourceDef at comptime — a typo in `test_fixtures/resource.zon`
// fails the build, not the test.
var pm = project.ProjectManager.init(allocator);
defer pm.deinit();
try pm.newProject("with_resources");

const proj = pm.current_project.?;
const a = proj.arena.allocator();
const resources = try a.alloc(project.ResourceDef, 1);
resources[0] = .{
.name = try a.dupe(u8, "sprites"),
.json = try a.dupe(u8, "assets/sprites.json"),
.texture = try a.dupe(u8, "assets/sprites.png"),
};
resources[0] = test_fixtures.ResourceFactory.build(.{});
proj.config.resources = resources;

try pm.saveProject(temp_dir);
Expand All @@ -1916,6 +1915,58 @@ pub const ProjectFileTests = struct {
try expect.toBeTrue(std.mem.eql(u8, loaded[0].texture, "assets/sprites.png"));
}

test "ProjectConfigFactory overrides round-trip through save + load" {
// Proof-of-value for the Factory pattern (precursor to #120's
// `project_settings_edit_save` triage): build a non-default
// ProjectConfig with several overrides via Factory.build,
// round-trip through saveProject/loadProject, assert every
// override survived. Replaces what would otherwise be a
// multi-line struct literal plus arena.dupe per string field.
const allocator = std.testing.allocator;
const temp_dir = try createTempDir(allocator);
defer deleteTempDir(allocator, temp_dir);

var pm = project.ProjectManager.init(allocator);
defer pm.deinit();
try pm.newProject("factory_overrides");

const proj = pm.current_project.?;
proj.config = test_fixtures.ProjectConfigFactory.build(.{
.name = "factory_overrides",
.title = "Factory Overrides",
.width = 1920,
.height = 1080,
.backend = .sokol,
.initial_scene = "splash",
.engine_version = "1.36.0",
});
Comment on lines +1934 to +1942

try pm.saveProject(temp_dir);

var pm2 = project.ProjectManager.init(allocator);
defer pm2.deinit();
try pm2.loadProject(temp_dir);

const cfg = pm2.current_project.?.config;
try expect.toBeTrue(std.mem.eql(u8, cfg.name, "factory_overrides"));
try expect.toBeTrue(std.mem.eql(u8, cfg.title, "Factory Overrides"));
try expect.equal(cfg.width, 1920);
try expect.equal(cfg.height, 1080);
try expect.equal(cfg.backend, .sokol);
try expect.toBeTrue(std.mem.eql(u8, cfg.initial_scene, "splash"));
try expect.toBeTrue(std.mem.eql(u8, cfg.engine_version, "1.36.0"));
// Non-overridden fields keep their ProjectConfigFactory
// defaults — proves the factory's defaults pass through
// saveProject + loadProject untouched.
try expect.equal(cfg.ecs, .zig_ecs);
try expect.equal(cfg.target_fps, 60);
try expect.toBeTrue(std.mem.eql(u8, cfg.description, ""));
try expect.toBeTrue(std.mem.eql(u8, cfg.core_version, "1.12.0"));
try expect.toBeTrue(std.mem.eql(u8, cfg.gfx_version, "1.10.0"));
try expect.toBeTrue(std.mem.eql(u8, cfg.assembler_version, "0.17.0"));
try expect.equal(cfg.resources.len, 0);
}

// Regression: ../flying-platform-labelle/project.labelle (and any
// project authored by the assembler / CLI) carries fields like
// `states`, `layers`, `plugins`, `gui`, `labelle_version`, etc. that
Expand Down
Loading