From 445ae9705cb22180d7eb6e53538f1b8f16a36e20 Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 19 Jun 2026 16:06:02 +0300 Subject: [PATCH] P5.8: add a macOS .app bundle smoke test to the corpus (closes the no-bundler-coverage gap) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The corpus had ZERO bundler coverage (the stream's named top risk). Add a `.build` `bundle` directive to the corpus runner: after a successful `aot` build it asserts each `expect` entry exists under the produced `.app` (repo-relative), then `rm -rf`s it. macOS-host only — the `.app` + codesign are Apple-specific, so the example is skipped on other hosts. `examples/1665-platform-macos-bundle-smoke.sx` sets `bundle_path`/`bundle_id` via a `#run` config; `default_pipeline` auto-bundles (build.sx imports the bundler, no explicit `on_build` needed). The directive asserts `Contents/MacOS`, `Contents/Info.plist`, `Contents/_CodeSignature`. Verified: passes on BOTH gates (the bundler runs on the legacy interp AND the VM), the `.app` is cleaned up, and a bad `expect` entry correctly fails (the check is not vacuous). Unit test + CLAUDE.md `.build`-directive docs updated. 706/0 both gates. --- CLAUDE.md | 6 +++ examples/1665-platform-macos-bundle-smoke.sx | 21 +++++++++ .../1665-platform-macos-bundle-smoke.build | 1 + .../1665-platform-macos-bundle-smoke.exit | 1 + .../1665-platform-macos-bundle-smoke.stderr | 1 + .../1665-platform-macos-bundle-smoke.stdout | 1 + src/corpus_run.test.zig | 43 +++++++++++++++++++ 7 files changed, 74 insertions(+) create mode 100644 examples/1665-platform-macos-bundle-smoke.sx create mode 100644 examples/expected/1665-platform-macos-bundle-smoke.build create mode 100644 examples/expected/1665-platform-macos-bundle-smoke.exit create mode 100644 examples/expected/1665-platform-macos-bundle-smoke.stderr create mode 100644 examples/expected/1665-platform-macos-bundle-smoke.stdout diff --git a/CLAUDE.md b/CLAUDE.md index e7b8b497..6f07e117 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -484,6 +484,12 @@ The optional `.build` JSON sidecar carries per-example directives in ir-only mode (its absence is a loud failure). This is how arch-pinned examples (e.g. x86_64 inline-asm) are tested on a non-matching dev host while still running end-to-end on a matching CI runner. +- `"bundle": { "app": "", "expect": ["Contents/MacOS", ...] }` — + bundle smoke test (requires `"aot": true`). After the `sx build` (which runs the + sx bundler via `default_pipeline`) the runner asserts each `expect` entry exists + under `app` (repo-relative), then `rm -rf`s the `app`. **macOS-host ONLY** — on any + other host the example is SKIPPED (the `.app` + `codesign` are Apple-specific). + Example: `examples/1665-platform-macos-bundle-smoke.sx`. ### Snapshot integrity diff --git a/examples/1665-platform-macos-bundle-smoke.sx b/examples/1665-platform-macos-bundle-smoke.sx new file mode 100644 index 00000000..b89d9ae6 --- /dev/null +++ b/examples/1665-platform-macos-bundle-smoke.sx @@ -0,0 +1,21 @@ +// macOS `.app` bundle smoke test — the corpus's first real bundler coverage. +// +// `default_pipeline` auto-bundles when `bundle_path` is set (build.sx imports the +// sx bundler, so no explicit `on_build` is needed). `sx build` runs the bundler +// after link, producing a signed `.app`. The `.build` `bundle` directive asserts +// the `.app` structure (`Contents/MacOS`, `Info.plist`, `_CodeSignature`) and then +// cleans it up. macOS-host ONLY — the directive skips the example on other hosts. + +#import "modules/std.sx"; + +configure :: () abi(.compiler) { + opts := build_options(); + opts.set_bundle_path(".sx-tmp/1665-platform-macos-bundle-smoke.app"); + opts.set_bundle_id("co.example.bundlesmoke"); +} +#run configure(); + +main :: () -> i32 { + print("bundle smoke ok\n"); + return 0; +} diff --git a/examples/expected/1665-platform-macos-bundle-smoke.build b/examples/expected/1665-platform-macos-bundle-smoke.build new file mode 100644 index 00000000..b048519e --- /dev/null +++ b/examples/expected/1665-platform-macos-bundle-smoke.build @@ -0,0 +1 @@ +{ "aot": true, "bundle": { "app": ".sx-tmp/1665-platform-macos-bundle-smoke.app", "expect": ["Contents/Info.plist", "Contents/MacOS", "Contents/_CodeSignature"] } } diff --git a/examples/expected/1665-platform-macos-bundle-smoke.exit b/examples/expected/1665-platform-macos-bundle-smoke.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/1665-platform-macos-bundle-smoke.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/1665-platform-macos-bundle-smoke.stderr b/examples/expected/1665-platform-macos-bundle-smoke.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1665-platform-macos-bundle-smoke.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/1665-platform-macos-bundle-smoke.stdout b/examples/expected/1665-platform-macos-bundle-smoke.stdout new file mode 100644 index 00000000..67231cbe --- /dev/null +++ b/examples/expected/1665-platform-macos-bundle-smoke.stdout @@ -0,0 +1 @@ +bundle smoke ok diff --git a/src/corpus_run.test.zig b/src/corpus_run.test.zig index b03a1410..053bf730 100644 --- a/src/corpus_run.test.zig +++ b/src/corpus_run.test.zig @@ -185,6 +185,17 @@ fn nameMatchesFilter(filter: []const u8, rel_path: []const u8) bool { const BuildConfig = struct { aot: bool = false, target: ?[]const u8 = null, + /// Bundle smoke-test directive (requires `aot`). After a successful build the + /// runner asserts each `expect` entry exists under `app` (repo-relative), then + /// `rm -rf`s the `app`. macOS-host ONLY (the `.app` + `codesign` are Apple) — + /// on any other host the example is SKIPPED (the bundler would take a different + /// per-OS branch / fail codesign). + bundle: ?BundleCheck = null, +}; + +const BundleCheck = struct { + app: []const u8, + expect: []const []const u8, }; fn parseBuildConfig(a: std.mem.Allocator, text: []const u8) !BuildConfig { @@ -334,6 +345,14 @@ fn sweepRoot( } else .{}; + // A bundle smoke test (`.app`/codesign) only makes sense on a macOS host — + // skip it elsewhere (the bundler branches per-OS / codesign is Apple-only). + if (cfg.bundle != null and builtin.target.os.tag != .macos) { + ran -= 1; + skipped += 1; + std.debug.print("[corpus-run] skip {s} (bundle smoke test — macOS host only)\n", .{name}); + continue; + } const is_aot = cfg.aot; // An example pinned to a non-host target cannot execute here; it routes @@ -401,6 +420,24 @@ fn sweepRoot( act_exit = termCode(exec_res.term); act_out = trimNl(try normalizeStd(a, exec_res.stdout)); act_err = trimNl(try normalizeStd(a, exec_res.stderr)); + + // Bundle smoke test: the `sx build` above ran the sx bundler + // (default_pipeline → bundle_main) and produced an `.app`. Assert + // its structure, then `rm -rf` it so it doesn't linger. + if (cfg.bundle) |bc| { + const app_abs = try std.fs.path.join(a, &.{ repo_root, bc.app }); + for (bc.expect) |entry| { + const entry_abs = try std.fs.path.join(a, &.{ app_abs, entry }); + std.Io.Dir.access(.cwd(), io, entry_abs, .{}) catch { + try failures.append(fail_gpa, try std.fmt.allocPrint(fail_gpa, "{s}: bundle missing '{s}' under {s}", .{ name, entry, bc.app })); + }; + } + _ = std.process.run(a, io, .{ + .argv = &.{ "/bin/rm", "-rf", app_abs }, + .cwd = .{ .path = repo_root }, + .timeout = deadline(io), + }) catch {}; + } } } else { // --- sx run --- @@ -572,6 +609,12 @@ test "parseBuildConfig: defaults, fields, unknown key" { try std.testing.expect(!tgt.aot); try std.testing.expectEqualStrings("x86_64-linux", tgt.target.?); + const bnd = try parseBuildConfig(a, "{ \"aot\": true, \"bundle\": { \"app\": \"x.app\", \"expect\": [\"Contents/MacOS\", \"Contents/Info.plist\"] } }"); + try std.testing.expect(bnd.aot); + try std.testing.expectEqualStrings("x.app", bnd.bundle.?.app); + try std.testing.expectEqual(@as(usize, 2), bnd.bundle.?.expect.len); + try std.testing.expectEqualStrings("Contents/Info.plist", bnd.bundle.?.expect[1]); + // Unknown key is a loud error, not a silent ignore. try std.testing.expectError(error.UnknownField, parseBuildConfig(a, "{ \"bogus\": 1 }")); }