P5.8: add a macOS .app bundle smoke test to the corpus (closes the no-bundler-coverage gap)
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.
This commit is contained in:
@@ -484,6 +484,12 @@ The optional `<name>.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": "<rel .app path>", "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
|
||||
|
||||
|
||||
21
examples/1665-platform-macos-bundle-smoke.sx
Normal file
21
examples/1665-platform-macos-bundle-smoke.sx
Normal file
@@ -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;
|
||||
}
|
||||
1
examples/expected/1665-platform-macos-bundle-smoke.build
Normal file
1
examples/expected/1665-platform-macos-bundle-smoke.build
Normal file
@@ -0,0 +1 @@
|
||||
{ "aot": true, "bundle": { "app": ".sx-tmp/1665-platform-macos-bundle-smoke.app", "expect": ["Contents/Info.plist", "Contents/MacOS", "Contents/_CodeSignature"] } }
|
||||
1
examples/expected/1665-platform-macos-bundle-smoke.exit
Normal file
1
examples/expected/1665-platform-macos-bundle-smoke.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
bundle smoke ok
|
||||
@@ -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 }"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user