From c7f5d211dc78b6a602cbd0948c5144db2eb3ffc3 Mon Sep 17 00:00:00 2001 From: rustytrees Date: Wed, 29 Jul 2026 17:16:29 -0400 Subject: [PATCH] build: add a single-binary test target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `zig build test` compiles one executable per file under `tests/` — 162 of them — and runs 162 fresh processes. That is slow to link, and on a host with a per-binary firewall or endpoint agent it produces a prompt storm per run, because every rebuild is a new executable the agent has not seen. `zig build test-one` links and runs exactly one binary over the same files: 2591 passed, 3 skipped, 0 failed in ~2m20s. `zig build test` is untouched and stays authoritative — one process per file is what actually isolates process-global state, and CI should keep using it. Sharing a process immediately surfaced one latent order dependency, which is fixed here rather than papered over: `custom_theme_test`'s "valid themes file" case asserts on captured CLI output, but `emitPrefixLine` early-returns while the module-global quiet flag is set. Run in its own binary the flag happens to be clear; run after a file that leaves `setQuiet(true)` behind, the capture is empty and the assertion compares against nothing. `boot()` already pins the colour state for exactly this reason — quiet was the one input it missed. That is worth noting on its own: the per-file layout hides this class of bug, and `just test-concurrent` only covers shared *fixture paths*, not shared in-process globals. This target is a cheap way to keep the second axis honest. --- build.zig | 26 ++++++ tests/all.zig | 180 ++++++++++++++++++++++++++++++++++++ tests/custom_theme_test.zig | 5 + 3 files changed, 211 insertions(+) create mode 100644 tests/all.zig diff --git a/build.zig b/build.zig index f64c44f6..c40c2e72 100644 --- a/build.zig +++ b/build.zig @@ -351,6 +351,23 @@ pub fn build(b: *std.Build) void { }); test_bin_step.dependOn(&install_lib_tests.step); + // Single-binary aggregate of every `tests/` file. `zig build test` links + // ~160 executables and spawns ~160 processes; this links and runs one. + // Useful for a fast local loop, and on hosts where a per-binary firewall + // or endpoint agent prompts once per new executable. `test` stays the + // authoritative target — it keeps one process per file, which is what + // isolates process-global state (see `tests/all.zig`). + const all_tests_module = b.createModule(.{ + .root_source_file = b.path("tests/all.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }); + all_tests_module.addIncludePath(b.path("vendor/")); + all_tests_module.addIncludePath(b.path("c/")); + all_tests_module.addOptions("version_string", version_options); + all_tests_module.addImport("malt", malt_lib); + // Shared test-only helpers — single module imported by every integration test. const test_io_mod = b.createModule(.{ .root_source_file = b.path("tests/test_io.zig"), @@ -360,6 +377,15 @@ pub fn build(b: *std.Build) void { }); test_io_mod.addImport("malt", malt_lib); + all_tests_module.addImport("test_io", test_io_mod); + const all_tests = b.addTest(.{ .name = "all_tests", .root_module = all_tests_module }); + const run_all_tests = b.addRunArtifact(all_tests); + // Throwaway prefix: `atomic.maltPrefixOrAbort()` otherwise falls back to + // the literal /opt/malt, i.e. the developer's real install. + run_all_tests.setEnvironmentVariable("MALT_PREFIX", "/tmp/malt-test-prefix"); + b.step("test-one", "Run every test in a single binary (one process)") + .dependOn(&run_all_tests.step); + @setEvalBranchQuota(30000); inline for (test_modules) |test_file| { // e.g. "tests/formula_test.zig" → "formula_test" (so each test binary diff --git a/tests/all.zig b/tests/all.zig new file mode 100644 index 00000000..9a23266f --- /dev/null +++ b/tests/all.zig @@ -0,0 +1,180 @@ +//! malt — single-binary aggregate of every integration test. +//! +//! `zig build test` compiles one executable per file under `tests/`, which is +//! ~160 fresh binaries and ~160 fresh processes per run. That is slow to link +//! and, on a machine with a per-binary firewall or endpoint agent, produces a +//! prompt storm that has nothing to do with what the tests are checking. +//! +//! This root pulls the same files into one module so `zig build test-one` +//! links and runs exactly one binary. Zig collects `test` blocks from every +//! file reachable from the root, so the referenced imports below are enough. +//! +//! Trade-off: one process means process-global state is shared. Tests that +//! `setenv`/`unsetenv` already restore in `defer`, and the runner is +//! sequential, so this holds today — but `zig build test` remains the +//! authoritative target, and it stays one-process-per-file. + +comptime { + _ = @import("formula_test.zig"); + _ = @import("deps_test.zig"); + _ = @import("macho_test.zig"); + _ = @import("store_test.zig"); + _ = @import("linker_test.zig"); + _ = @import("rollback_test.zig"); + _ = @import("run_test.zig"); + _ = @import("version_update_test.zig"); + _ = @import("version_notify_test.zig"); + _ = @import("origin_test.zig"); + _ = @import("release_test.zig"); + _ = @import("verify_test.zig"); + _ = @import("swap_test.zig"); + _ = @import("cleanup_test.zig"); + _ = @import("cask_test.zig"); + _ = @import("cellar_test.zig"); + _ = @import("progress_test.zig"); + _ = @import("progress_e2e_test.zig"); + _ = @import("completions_test.zig"); + _ = @import("flag_drift_test.zig"); + _ = @import("cask_revision_invariant_test.zig"); + _ = @import("backup_test.zig"); + _ = @import("purge_test.zig"); + _ = @import("purge_output_test.zig"); + _ = @import("purge_json_test.zig"); + _ = @import("install_test.zig"); + _ = @import("install_parse_cache_test.zig"); + _ = @import("archive_extract_symlink_test.zig"); + _ = @import("deps_leak_test.zig"); + _ = @import("api_test.zig"); + _ = @import("offline_mode_test.zig"); + _ = @import("clonefile_test.zig"); + _ = @import("dsl_lexer_test.zig"); + _ = @import("dsl_parser_test.zig"); + _ = @import("dsl_sandbox_test.zig"); + _ = @import("dsl_interpreter_test.zig"); + _ = @import("dsl_install_symlink_test.zig"); + _ = @import("doctor_ssl_test.zig"); + _ = @import("db_schema_v2_test.zig"); + _ = @import("bundle_brewfile_test.zig"); + _ = @import("services_plist_test.zig"); + _ = @import("services_test.zig"); + _ = @import("bundle_test.zig"); + _ = @import("bundle_cleanup_test.zig"); + _ = @import("atomic_test.zig"); + _ = @import("lock_test.zig"); + _ = @import("install_warm_path_test.zig"); + _ = @import("tap_test.zig"); + _ = @import("fallback_log_test.zig"); + _ = @import("help_test.zig"); + _ = @import("archive_test.zig"); + _ = @import("ghcr_test.zig"); + _ = @import("ghcr_401_retry_test.zig"); + _ = @import("net_get_to_writer_test.zig"); + _ = @import("bottle_download_test.zig"); + _ = @import("linker_core_test.zig"); + _ = @import("supervisor_pure_test.zig"); + _ = @import("install_pure_test.zig"); + _ = @import("dsl_builtins_test.zig"); + _ = @import("ruby_subprocess_test.zig"); + _ = @import("ruby_module_boundaries_test.zig"); + _ = @import("pins_test.zig"); + _ = @import("sandbox_macos_test.zig"); + _ = @import("services_validate_test.zig"); + _ = @import("spawn_invariant_test.zig"); + _ = @import("child_test.zig"); + _ = @import("net_client_test.zig"); + _ = @import("net_client_pool_test.zig"); + _ = @import("net_redirect_auth_test.zig"); + _ = @import("http_inject_test.zig"); + _ = @import("term_sanitize_test.zig"); + _ = @import("perms_test.zig"); + _ = @import("doctor_render_test.zig"); + _ = @import("doctor_fix_test.zig"); + _ = @import("cli_tap_test.zig"); + _ = @import("cli_services_test.zig"); + _ = @import("install_execute_test.zig"); + _ = @import("install_sink_test.zig"); + _ = @import("install_ambiguity_test.zig"); + _ = @import("install_download_only_test.zig"); + _ = @import("install_idempotent_test.zig"); + _ = @import("reinstall_test.zig"); + _ = @import("no_readall_in_loop_test.zig"); + _ = @import("test_io_primitives_test.zig"); + _ = @import("no_io_mod_in_src_test.zig"); + _ = @import("no_cli_in_core_test.zig"); + _ = @import("forge_purity_test.zig"); + _ = @import("no_main_reach_in_cli_test.zig"); + _ = @import("ui_color_theme_test.zig"); + _ = @import("custom_theme_test.zig"); + _ = @import("install_local_test.zig"); + _ = @import("cask_extra_test.zig"); + _ = @import("cask_font_install_test.zig"); + _ = @import("cask_font_uninstall_test.zig"); + _ = @import("cask_font_rollback_test.zig"); + _ = @import("cask_resolve_test.zig"); + _ = @import("dsl_interpreter_extra_test.zig"); + _ = @import("list_test.zig"); + _ = @import("search_test.zig"); + _ = @import("info_test.zig"); + _ = @import("uses_test.zig"); + _ = @import("which_test.zig"); + _ = @import("output_test.zig"); + _ = @import("ndjson_test.zig"); + _ = @import("worker_arena_test.zig"); + _ = @import("migrate_smoke_test.zig"); + _ = @import("migrate_manifest_test.zig"); + _ = @import("upgrade_test.zig"); + _ = @import("hash_test.zig"); + _ = @import("patcher_test.zig"); + _ = @import("pin_test.zig"); + _ = @import("shellenv_test.zig"); + _ = @import("outdated_test.zig"); + _ = @import("uninstall_test.zig"); + _ = @import("purge_scopes_test.zig"); + _ = @import("link_cli_test.zig"); + _ = @import("restore_cli_test.zig"); + _ = @import("search_cli_test.zig"); + _ = @import("doctor_dispatch_test.zig"); + _ = @import("doctor_cask_history_test.zig"); + _ = @import("doctor_tap_forge_test.zig"); + _ = @import("doctor_json_test.zig"); + _ = @import("info_cli_test.zig"); + _ = @import("backup_cli_test.zig"); + _ = @import("run_cli_test.zig"); + _ = @import("bundle_cli_test.zig"); + _ = @import("version_update_cli_test.zig"); + _ = @import("upgrade_cli_test.zig"); + _ = @import("list_cli_test.zig"); + _ = @import("tap_cli_test.zig"); + _ = @import("uses_cli_test.zig"); + _ = @import("deps_cli_exec_test.zig"); + _ = @import("text_replace_test.zig"); + _ = @import("bottle_test.zig"); + _ = @import("cleanup_cli_test.zig"); + _ = @import("install_keg_from_bottle_test.zig"); + _ = @import("install_pool_test.zig"); + _ = @import("tap_head_etag_integration_test.zig"); + _ = @import("tap_raw_rb_auth_test.zig"); + _ = @import("tap_gitlab_resolution_test.zig"); + _ = @import("tap_gitea_resolution_test.zig"); + _ = @import("tap_gogs_resolution_test.zig"); + _ = @import("tap_root_layout_resolution_test.zig"); + _ = @import("tap_pin_forge_test.zig"); + _ = @import("tap_resolve_error_test.zig"); + _ = @import("linker_isolation_test.zig"); + _ = @import("install_isolation_test.zig"); + _ = @import("doctor_isolation_test.zig"); + _ = @import("tui_term_test.zig"); + _ = @import("tui_keys_test.zig"); + _ = @import("tui_layout_test.zig"); + _ = @import("tui_scroll_list_test.zig"); + _ = @import("tui_app_test.zig"); + _ = @import("tui_spawn_test.zig"); + _ = @import("tui_installed_tab_test.zig"); + _ = @import("tui_outdated_test.zig"); + _ = @import("tui_services_test.zig"); + _ = @import("tui_doctor_test.zig"); + _ = @import("tui_search_test.zig"); + _ = @import("tui_purity_test.zig"); + _ = @import("tui_selection_theme_test.zig"); + _ = @import("tui_outdated_warm_read_test.zig"); +} diff --git a/tests/custom_theme_test.zig b/tests/custom_theme_test.zig index 49cd815c..04c096a1 100644 --- a/tests/custom_theme_test.zig +++ b/tests/custom_theme_test.zig @@ -81,6 +81,11 @@ fn boot(fx: *Fixture) color.InstallResult { color.setForTest(true, false); // colour on so CLI output emits SGR color.setTruecolorForTest(true); color.setBackgroundForTest(.unknown); + // `emitPrefixLine` early-returns while the module-global quiet flag is set, + // so a preceding test that leaves `setQuiet(true)` behind makes the CLI + // assertion below compare against an empty capture. Pin it for the same + // reason the colour state is pinned: this test is about what gets emitted. + output.setQuiet(false); var buf: [malt.custom_theme.max_file_bytes]u8 = undefined; const bytes = theme_file.read(dbg_io, environ, &buf);