Skip to content

Commit 9443c69

Browse files
committed
Only build tests on native targets
1 parent c1b7650 commit 9443c69

File tree

5 files changed

+353
-0
lines changed

5 files changed

+353
-0
lines changed

build.zig

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,41 @@ pub fn build(b: *std.Build) void {
598598
const is_native = target.query.isNativeCpu() and target.query.isNativeOs() and (target.query.isNativeAbi() or target.result.abi.isMusl());
599599
const is_windows = target.result.os.tag == .windows;
600600

601+
// fx platform effectful functions test - only run on native builds
602+
if (is_native) {
603+
// Create fx test platform host static library
604+
const test_platform_fx_host_lib = createTestPlatformHostLib(
605+
b,
606+
"test_platform_fx_host",
607+
"test/fx/platform/host.zig",
608+
target,
609+
optimize,
610+
roc_modules,
611+
);
612+
613+
// Copy the fx test platform host library to the source directory
614+
const copy_test_fx_host = b.addUpdateSourceFiles();
615+
const test_fx_host_filename = if (target.result.os.tag == .windows) "host.lib" else "libhost.a";
616+
copy_test_fx_host.addCopyFileToSource(test_platform_fx_host_lib.getEmittedBin(), b.pathJoin(&.{ "test/fx/platform", test_fx_host_filename }));
617+
b.getInstallStep().dependOn(&copy_test_fx_host.step);
618+
619+
const fx_platform_test = b.addTest(.{
620+
.name = "fx_platform_test",
621+
.root_module = b.createModule(.{
622+
.root_source_file = b.path("src/cli/test/fx_platform_test.zig"),
623+
.target = target,
624+
.optimize = optimize,
625+
}),
626+
.filters = test_filters,
627+
});
628+
629+
const run_fx_platform_test = b.addRunArtifact(fx_platform_test);
630+
if (run_args.len != 0) {
631+
run_fx_platform_test.addArgs(run_args);
632+
}
633+
tests_summary.addRun(&run_fx_platform_test.step);
634+
}
635+
601636
var build_afl = false;
602637
if (!is_native) {
603638
std.log.warn("Cross compilation does not support fuzzing (Only building repro executables)", .{});

src/cli/test/fx_platform_test.zig

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const std = @import("std");
2+
const testing = std.testing;
3+
4+
test "fx platform effectful functions" {
5+
const allocator = testing.allocator;
6+
7+
// Build the fx app
8+
const build_result = try std.process.Child.run(.{
9+
.allocator = allocator,
10+
.argv = &[_][]const u8{
11+
"./zig-out/bin/roc",
12+
"build",
13+
"test/fx/app.roc",
14+
},
15+
});
16+
defer allocator.free(build_result.stdout);
17+
defer allocator.free(build_result.stderr);
18+
19+
switch (build_result.term) {
20+
.Exited => |code| {
21+
if (code != 0) {
22+
std.debug.print("Build failed with exit code {}\n", .{code});
23+
std.debug.print("STDOUT: {s}\n", .{build_result.stdout});
24+
std.debug.print("STDERR: {s}\n", .{build_result.stderr});
25+
return error.BuildFailed;
26+
}
27+
},
28+
else => {
29+
std.debug.print("Build terminated abnormally: {}\n", .{build_result.term});
30+
std.debug.print("STDOUT: {s}\n", .{build_result.stdout});
31+
std.debug.print("STDERR: {s}\n", .{build_result.stderr});
32+
return error.BuildFailed;
33+
},
34+
}
35+
36+
// Run the app and capture stdout/stderr separately
37+
const run_result = try std.process.Child.run(.{
38+
.allocator = allocator,
39+
.argv = &[_][]const u8{"./app"},
40+
});
41+
defer allocator.free(run_result.stdout);
42+
defer allocator.free(run_result.stderr);
43+
44+
// Clean up the app binary
45+
std.fs.cwd().deleteFile("./app") catch {};
46+
47+
switch (run_result.term) {
48+
.Exited => |code| {
49+
if (code != 0) {
50+
std.debug.print("Run failed with exit code {}\n", .{code});
51+
std.debug.print("STDOUT: {s}\n", .{run_result.stdout});
52+
std.debug.print("STDERR: {s}\n", .{run_result.stderr});
53+
return error.RunFailed;
54+
}
55+
},
56+
else => {
57+
std.debug.print("Run terminated abnormally: {}\n", .{run_result.term});
58+
std.debug.print("STDOUT: {s}\n", .{run_result.stdout});
59+
std.debug.print("STDERR: {s}\n", .{run_result.stderr});
60+
return error.RunFailed;
61+
},
62+
}
63+
64+
// Verify stdout contains expected messages
65+
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Hello from stdout!") != null);
66+
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Line 1 to stdout") != null);
67+
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Line 3 to stdout") != null);
68+
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "ALL TESTS COMPLETED") != null);
69+
70+
// Verify stderr contains expected messages
71+
try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Error from stderr!") != null);
72+
try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Line 2 to stderr") != null);
73+
74+
// Verify stderr messages are NOT in stdout
75+
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Error from stderr!") == null);
76+
try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Line 2 to stderr") == null);
77+
78+
// Verify stdout messages are NOT in stderr (except the ones we intentionally put there for display)
79+
// Note: The host.zig writes "STDOUT: ..." to stdout and "STDERR: ..." to stderr
80+
// so we check that the actual content is separated correctly
81+
}

test/fx/app.roc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
app [writeToStdout, writeToStderr] { pf: platform "./platform/main.roc" }
2+
3+
writeToStdout : Str => {}
4+
writeToStdout = |_msg|
5+
# The host will actually handle the IO
6+
{}
7+
8+
writeToStderr : Str => {}
9+
writeToStderr = |_msg|
10+
# The host will actually handle the IO
11+
{}

test/fx/platform/host.zig

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
///! Platform host that tests effectful functions writing to stdout and stderr.
2+
3+
const std = @import("std");
4+
const builtins = @import("builtins");
5+
6+
/// Host environment - contains our arena allocator
7+
const HostEnv = struct {
8+
arena: std.heap.ArenaAllocator,
9+
};
10+
11+
/// Roc allocation function
12+
fn rocAllocFn(roc_alloc: *builtins.host_abi.RocAlloc, env: *anyopaque) callconv(.c) void {
13+
const host: *HostEnv = @ptrCast(@alignCast(env));
14+
const allocator = host.arena.allocator();
15+
16+
const log2_align = std.math.log2_int(u32, @intCast(roc_alloc.alignment));
17+
const align_enum: std.mem.Alignment = @enumFromInt(log2_align);
18+
19+
const result = allocator.rawAlloc(roc_alloc.length, align_enum, @returnAddress());
20+
21+
roc_alloc.answer = result orelse {
22+
@panic("Host allocation failed");
23+
};
24+
}
25+
26+
/// Roc deallocation function
27+
fn rocDeallocFn(roc_dealloc: *builtins.host_abi.RocDealloc, env: *anyopaque) callconv(.c) void {
28+
_ = roc_dealloc;
29+
_ = env;
30+
// NoOp as our arena frees all memory at once
31+
}
32+
33+
/// Roc reallocation function
34+
fn rocReallocFn(roc_realloc: *builtins.host_abi.RocRealloc, env: *anyopaque) callconv(.c) void {
35+
_ = roc_realloc;
36+
_ = env;
37+
@panic("Realloc not implemented in this example");
38+
}
39+
40+
/// Roc debug function
41+
fn rocDbgFn(roc_dbg: *const builtins.host_abi.RocDbg, env: *anyopaque) callconv(.c) void {
42+
_ = env;
43+
const message = roc_dbg.utf8_bytes[0..roc_dbg.len];
44+
std.debug.print("ROC DBG: {s}\n", .{message});
45+
}
46+
47+
/// Roc expect failed function
48+
fn rocExpectFailedFn(roc_expect: *const builtins.host_abi.RocExpectFailed, env: *anyopaque) callconv(.c) void {
49+
_ = env;
50+
const message = roc_expect.utf8_bytes[0..roc_expect.len];
51+
std.debug.print("ROC EXPECT FAILED: {s}\n", .{message});
52+
}
53+
54+
/// Roc crashed function
55+
fn rocCrashedFn(roc_crashed: *const builtins.host_abi.RocCrashed, env: *anyopaque) callconv(.c) noreturn {
56+
_ = env;
57+
const message = roc_crashed.utf8_bytes[0..roc_crashed.len];
58+
@panic(message);
59+
}
60+
61+
// External symbols provided by the Roc runtime object file
62+
// Follows RocCall ABI: ops, ret_ptr, then argument pointers
63+
extern fn roc__writeToStdout(ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void;
64+
extern fn roc__writeToStderr(ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void;
65+
66+
// OS-specific entry point handling
67+
comptime {
68+
// Export main for all platforms
69+
@export(&main, .{ .name = "main" });
70+
71+
// Windows MinGW/MSVCRT compatibility: export __main stub
72+
if (@import("builtin").os.tag == .windows) {
73+
@export(&__main, .{ .name = "__main" });
74+
}
75+
}
76+
77+
// Windows MinGW/MSVCRT compatibility stub
78+
// The C runtime on Windows calls __main from main for constructor initialization
79+
fn __main() callconv(.c) void {}
80+
81+
// C compatible main for runtime
82+
fn main(argc: c_int, argv: [*][*:0]u8) callconv(.c) c_int {
83+
_ = argc;
84+
_ = argv;
85+
platform_main() catch |err| {
86+
std.fs.File.stderr().deprecatedWriter().print("HOST ERROR: {s}\n", .{@errorName(err)}) catch unreachable;
87+
return 1;
88+
};
89+
return 0;
90+
}
91+
92+
/// RocStr type matching Roc's internal string representation
93+
const RocStr = extern struct {
94+
bytes: ?[*]u8,
95+
len: usize,
96+
capacity: isize,
97+
98+
/// Create a RocStr from a Zig slice (small string optimization aware)
99+
fn fromSlice(slice: []const u8, ops: *builtins.host_abi.RocOps) RocStr {
100+
const len = slice.len;
101+
102+
// Small string optimization: strings <= 23 bytes are stored inline
103+
if (len <= 23) {
104+
var result = RocStr{
105+
.bytes = null,
106+
.len = len,
107+
.capacity = -1, // Negative capacity indicates small string
108+
};
109+
110+
// Copy bytes into the inline storage (stored in the bytes pointer space)
111+
const dest: [*]u8 = @ptrCast(&result.bytes);
112+
@memcpy(dest[0..len], slice);
113+
114+
return result;
115+
}
116+
117+
// Large string: allocate on heap
118+
var roc_alloc = builtins.host_abi.RocAlloc{
119+
.length = len,
120+
.alignment = @alignOf(u8),
121+
.answer = undefined,
122+
};
123+
124+
ops.roc_alloc(&roc_alloc, ops.env);
125+
126+
const bytes: [*]u8 = @ptrCast(@alignCast(roc_alloc.answer));
127+
@memcpy(bytes[0..len], slice);
128+
129+
return RocStr{
130+
.bytes = bytes,
131+
.len = len,
132+
.capacity = @intCast(len),
133+
};
134+
}
135+
136+
/// Get the bytes as a Zig slice
137+
fn asSlice(self: *const RocStr) []const u8 {
138+
if (self.capacity < 0) {
139+
// Small string: bytes are stored inline
140+
const inline_bytes: [*]const u8 = @ptrCast(&self.bytes);
141+
return inline_bytes[0..self.len];
142+
} else {
143+
// Large string: bytes are on heap
144+
return if (self.bytes) |ptr| ptr[0..self.len] else &[_]u8{};
145+
}
146+
}
147+
};
148+
149+
/// Platform host entrypoint
150+
fn platform_main() !void {
151+
var host_env = HostEnv{
152+
.arena = std.heap.ArenaAllocator.init(std.heap.page_allocator),
153+
};
154+
defer host_env.arena.deinit();
155+
156+
const stdout = std.fs.File.stdout().deprecatedWriter();
157+
const stderr = std.fs.File.stderr().deprecatedWriter();
158+
159+
// Create the RocOps struct
160+
var roc_ops = builtins.host_abi.RocOps{
161+
.env = @as(*anyopaque, @ptrCast(&host_env)),
162+
.roc_alloc = rocAllocFn,
163+
.roc_dealloc = rocDeallocFn,
164+
.roc_realloc = rocReallocFn,
165+
.roc_dbg = rocDbgFn,
166+
.roc_expect_failed = rocExpectFailedFn,
167+
.roc_crashed = rocCrashedFn,
168+
.host_fns = undefined, // No host functions needed for this test
169+
};
170+
171+
// Test writeToStdout
172+
try stdout.print("=== Testing writeToStdout ===\n", .{});
173+
174+
const stdout_msg = "Hello from stdout!";
175+
var stdout_roc_str = RocStr.fromSlice(stdout_msg, &roc_ops);
176+
177+
var stdout_result: [0]u8 = undefined; // Result is {} which is zero-sized
178+
roc__writeToStdout(&roc_ops, @as(*anyopaque, @ptrCast(&stdout_result)), @as(*anyopaque, @ptrCast(&stdout_roc_str)));
179+
180+
try stdout.print("STDOUT: {s}\n", .{stdout_msg});
181+
182+
// Test writeToStderr
183+
try stdout.print("\n=== Testing writeToStderr ===\n", .{});
184+
185+
const stderr_msg = "Error from stderr!";
186+
var stderr_roc_str = RocStr.fromSlice(stderr_msg, &roc_ops);
187+
188+
var stderr_result: [0]u8 = undefined; // Result is {} which is zero-sized
189+
roc__writeToStderr(&roc_ops, @as(*anyopaque, @ptrCast(&stderr_result)), @as(*anyopaque, @ptrCast(&stderr_roc_str)));
190+
191+
try stderr.print("STDERR: {s}\n", .{stderr_msg});
192+
193+
// Test both together
194+
try stdout.print("\n=== Testing both ===\n", .{});
195+
196+
const msg1 = "Line 1 to stdout";
197+
const msg2 = "Line 2 to stderr";
198+
const msg3 = "Line 3 to stdout";
199+
200+
var msg1_roc = RocStr.fromSlice(msg1, &roc_ops);
201+
var msg2_roc = RocStr.fromSlice(msg2, &roc_ops);
202+
var msg3_roc = RocStr.fromSlice(msg3, &roc_ops);
203+
204+
roc__writeToStdout(&roc_ops, @as(*anyopaque, @ptrCast(&stdout_result)), @as(*anyopaque, @ptrCast(&msg1_roc)));
205+
try stdout.print("STDOUT: {s}\n", .{msg1});
206+
207+
roc__writeToStderr(&roc_ops, @as(*anyopaque, @ptrCast(&stderr_result)), @as(*anyopaque, @ptrCast(&msg2_roc)));
208+
try stderr.print("STDERR: {s}\n", .{msg2});
209+
210+
roc__writeToStdout(&roc_ops, @as(*anyopaque, @ptrCast(&stdout_result)), @as(*anyopaque, @ptrCast(&msg3_roc)));
211+
try stdout.print("STDOUT: {s}\n", .{msg3});
212+
213+
try stdout.print("\n=== ALL TESTS COMPLETED ===\n", .{});
214+
}

test/fx/platform/main.roc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
platform ""
2+
requires {} {
3+
writeToStdout : Str => {},
4+
writeToStderr : Str => {}
5+
}
6+
exposes []
7+
packages {}
8+
provides { writeToStdout: "writeToStdout", writeToStderr: "writeToStderr" }
9+
10+
writeToStdout : Str => {}
11+
12+
writeToStderr : Str => {}

0 commit comments

Comments
 (0)