tests: translate-c and run-translated-c to the test harness

This commit is contained in:
Veikka Tuominen 2023-10-14 22:02:32 +03:00
parent 58b07ea14f
commit e765495b11
17 changed files with 338 additions and 71 deletions

View File

@ -17,12 +17,14 @@ target: CrossTarget,
optimize: std.builtin.OptimizeMode,
output_file: std.Build.GeneratedFile,
link_libc: bool,
use_clang: bool,
pub const Options = struct {
source_file: std.Build.LazyPath,
target: CrossTarget,
optimize: std.builtin.OptimizeMode,
link_libc: bool = true,
use_clang: bool = true,
};
pub fn create(owner: *std.Build, options: Options) *TranslateC {
@ -43,6 +45,7 @@ pub fn create(owner: *std.Build, options: Options) *TranslateC {
.optimize = options.optimize,
.output_file = std.Build.GeneratedFile{ .step = &self.step },
.link_libc = options.link_libc,
.use_clang = options.use_clang,
};
source.addStepDependencies(&self.step);
return self;
@ -130,6 +133,9 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
if (self.link_libc) {
try argv_list.append("-lc");
}
if (!self.use_clang) {
try argv_list.append("-fno-clang");
}
try argv_list.append("--listen=-");

View File

@ -9,7 +9,7 @@ If you want it to be run with `zig test` and match expected error messages:
```zig
// error
// is_test=1
// is_test=true
//
// :4:13: error: 'try' outside function scope
```
@ -22,6 +22,33 @@ This will do `zig run` on the code and expect exit code 0.
// run
```
## Translate-c
If you want to test translating C code to Zig use `translate-c`:
```c
// translate-c
// c_frontend=aro,clang
// target=x86_64-linux
//
// pub const foo = 1;
// pub const immediately_after_foo = 2;
//
// pub const somewhere_else_in_the_file = 3:
```
## Run Translated C
If you want to test translating C code to Zig and then executing it use `run-translated-c`:
```c
// run-translated-c
// c_frontend=aro,clang
// target=x86_64-linux
//
// Hello world!
```
## Incremental Compilation
Make multiple files that have ".", and then an integer, before the ".zig"

View File

@ -6,6 +6,6 @@ test "Crash" {
// error
// backend=stage2
// target=native
// is_test=1
// is_test=true
//
// :1:11: error: use of undeclared identifier 'B'

View File

@ -4,7 +4,7 @@ test "thingy" {}
// error
// backend=stage2
// target=native
// is_test=1
// is_test=true
//
// :1:6: error: duplicate test name: test.thingy
// :2:6: note: other test here

View File

@ -9,6 +9,6 @@ test "1" {
// error
// backend=stage2
// target=native
// is_test=1
// is_test=true
//
// :2:12: error: use of undeclared identifier 'Q'

View File

@ -5,6 +5,6 @@ test "example" {
// error
// backend=stage2
// target=native
// is_test=1
// is_test=true
//
// :2:12: error: expected type 'anyerror!void', found 'comptime_int'

View File

@ -6,7 +6,7 @@ test "enum" {
// error
// backend=stage2
// target=native
// is_test=1
// is_test=true
//
// :3:9: error: no field with value '@enumFromInt(5)' in enum 'test.enum.E'
// :2:15: note: declared here

View File

@ -9,7 +9,7 @@ pub fn main() void {
// run
// backend=llvm
// target=x86_64-linux-gnu
// link_libc=1
// link_libc=true
//
// f64: 2.000000
// f32: 10.000000

View File

@ -13,6 +13,6 @@ fn foo(comptime info: std.builtin.Type) !void {
}
// run
// is_test=1
// is_test=true
// backend=llvm
//

View File

@ -7,7 +7,7 @@ pub fn main() void {
// run
// backend=llvm
// target=x86_64-linux,x86_64-macos
// link_libc=1
// link_libc=true
//
// hello world!
//

View File

@ -0,0 +1,11 @@
#include <stdlib.h>
int main(void) {
int i = 0;
*&i = 42;
if (i != 42) abort();
return 0;
}
// run-translated-c
// c_frontend=clang
// link_libc=true

View File

@ -0,0 +1,16 @@
enum Foo {
FooA = 2,
FooB = 5,
Foo1,
};
// translate-c
// target=x86_64-windows-msvc
// c_frontend=clang
//
// pub const FooA: c_int = 2;
// pub const FooB: c_int = 5;
// pub const Foo1: c_int = 6;
// pub const enum_Foo = c_int;
//
// pub const Foo = enum_Foo;

View File

@ -0,0 +1,16 @@
enum Foo {
FooA = 2,
FooB = 5,
Foo1,
};
// translate-c
// target=x86_64-linux
// c_frontend=clang,aro
//
// pub const FooA: c_int = 2;
// pub const FooB: c_int = 5;
// pub const Foo1: c_int = 6;
// pub const enum_Foo = c_uint;
//
// pub const Foo = enum_Foo;

View File

@ -8,6 +8,6 @@ test "@unionInit on union w/ tag but no fields" {
}
// error
// is_test=1
// is_test=true
//
// :4:13: error: 'try' outside function scope

View File

@ -2,17 +2,14 @@ const std = @import("std");
const tests = @import("tests.zig");
const nl = if (@import("builtin").os.tag == .windows) "\r\n" else "\n";
pub fn addCases(cases: *tests.RunTranslatedCContext) void {
cases.add("dereference address of",
\\#include <stdlib.h>
\\int main(void) {
\\ int i = 0;
\\ *&i = 42;
\\ if (i != 42) abort();
\\ return 0;
\\}
, "");
// *********************************************************
// * *
// * DO NOT ADD NEW CASES HERE *
// * instead add a file to test/cases/run_translated_c *
// * *
// *********************************************************
pub fn addCases(cases: *tests.RunTranslatedCContext) void {
cases.add("division of floating literals",
\\#define _NO_CRT_STDIO_INLINE 1
\\#include <stdio.h>

View File

@ -1,6 +1,7 @@
gpa: Allocator,
arena: Allocator,
cases: std.ArrayList(Case),
translate: std.ArrayList(Translate),
incremental_cases: std.ArrayList(IncrementalCase),
pub const IncrementalCase = struct {
@ -36,7 +37,7 @@ pub const Update = struct {
Execution: []const u8,
/// A header update compiles the input with the equivalent of
/// `-femit-h` and tests the produced header against the
/// expected result
/// expected result.
Header: []const u8,
},
@ -61,6 +62,11 @@ pub const Backend = enum {
llvm,
};
pub const CFrontend = enum {
clang,
aro,
};
/// A `Case` consists of a list of `Update`. The same `Compilation` is used for each
/// update, so each update's source is treated as a single file being
/// updated by the test harness and incrementally compiled.
@ -143,6 +149,25 @@ pub const Case = struct {
}
};
pub const Translate = struct {
/// The name of the test case. This is shown if a test fails, and
/// otherwise ignored.
name: []const u8,
input: [:0]const u8,
target: CrossTarget,
link_libc: bool,
c_frontend: CFrontend,
kind: union(enum) {
/// Translate the input, run it and check that it
/// outputs the expected text.
run: []const u8,
/// Translate the input and check that it contains
/// the expected lines of code.
translate: []const []const u8,
},
};
pub fn addExe(
ctx: *Cases,
name: []const u8,
@ -346,9 +371,12 @@ pub fn addCompile(
pub fn addFromDir(ctx: *Cases, dir: std.fs.IterableDir) void {
var current_file: []const u8 = "none";
ctx.addFromDirInner(dir, &current_file) catch |err| {
std.debug.panic("test harness failed to process file '{s}': {s}\n", .{
current_file, @errorName(err),
});
std.debug.panicExtra(
@errorReturnTrace(),
@returnAddress(),
"test harness failed to process file '{s}': {s}\n",
.{ current_file, @errorName(err) },
);
};
}
@ -395,10 +423,44 @@ fn addFromDirInner(
const backends = try manifest.getConfigForKeyAlloc(ctx.arena, "backend", Backend);
const targets = try manifest.getConfigForKeyAlloc(ctx.arena, "target", CrossTarget);
const c_frontends = try manifest.getConfigForKeyAlloc(ctx.arena, "c_frontend", CFrontend);
const is_test = try manifest.getConfigForKeyAssertSingle("is_test", bool);
const link_libc = try manifest.getConfigForKeyAssertSingle("link_libc", bool);
const output_mode = try manifest.getConfigForKeyAssertSingle("output_mode", std.builtin.OutputMode);
if (manifest.type == .translate_c) {
for (c_frontends) |c_frontend| {
for (targets) |target| {
const output = try manifest.trailingLinesSplit(ctx.arena);
try ctx.translate.append(.{
.name = std.fs.path.stem(filename),
.c_frontend = c_frontend,
.target = target,
.link_libc = link_libc,
.input = src,
.kind = .{ .translate = output },
});
}
}
continue;
}
if (manifest.type == .run_translated_c) {
for (c_frontends) |c_frontend| {
for (targets) |target| {
const output = try manifest.trailingSplit(ctx.arena);
try ctx.translate.append(.{
.name = std.fs.path.stem(filename),
.c_frontend = c_frontend,
.target = target,
.link_libc = link_libc,
.input = src,
.kind = .{ .run = output },
});
}
}
continue;
}
var cases = std.ArrayList(usize).init(ctx.arena);
// Cross-product to get all possible test combinations
@ -439,21 +501,15 @@ fn addFromDirInner(
case.addCompile(src);
},
.@"error" => {
const errors = try manifest.trailingAlloc(ctx.arena);
const errors = try manifest.trailingLines(ctx.arena);
case.addError(src, errors);
},
.run => {
var output = std.ArrayList(u8).init(ctx.arena);
var trailing_it = manifest.trailing();
while (trailing_it.next()) |line| {
try output.appendSlice(line);
try output.append('\n');
}
if (output.items.len > 0) {
try output.resize(output.items.len - 1);
}
case.addCompareOutput(src, try output.toOwnedSlice());
const output = try manifest.trailingSplit(ctx.arena);
case.addCompareOutput(src, output);
},
.translate_c => @panic("c_frontend specified for compile case"),
.run_translated_c => @panic("c_frontend specified for compile case"),
.cli => @panic("TODO cli tests"),
}
}
@ -468,6 +524,7 @@ pub fn init(gpa: Allocator, arena: Allocator) Cases {
return .{
.gpa = gpa,
.cases = std.ArrayList(Case).init(gpa),
.translate = std.ArrayList(Translate).init(gpa),
.incremental_cases = std.ArrayList(IncrementalCase).init(gpa),
.arena = arena,
};
@ -482,7 +539,7 @@ pub fn lowerToBuildSteps(
incremental_exe: *std.Build.Step.Compile,
) void {
const host = std.zig.system.NativeTargetInfo.detect(.{}) catch |err|
std.debug.panic("unable to detect notive host: {s}\n", .{@errorName(err)});
std.debug.panic("unable to detect native host: {s}\n", .{@errorName(err)});
for (self.incremental_cases.items) |incr_case| {
if (true) {
@ -589,7 +646,7 @@ pub fn lowerToBuildSteps(
.Execution => |expected_stdout| no_exec: {
const run = if (case.target.ofmt == .c) run_step: {
const target_info = std.zig.system.NativeTargetInfo.detect(case.target) catch |err|
std.debug.panic("unable to detect notive host: {s}\n", .{@errorName(err)});
std.debug.panic("unable to detect target host: {s}\n", .{@errorName(err)});
if (host.getExternalExecutor(&target_info, .{ .link_libc = true }) != .native) {
// We wouldn't be able to run the compiled C code.
break :no_exec;
@ -623,6 +680,68 @@ pub fn lowerToBuildSteps(
.Header => @panic("TODO"),
}
}
for (self.translate.items) |*case| switch (case.kind) {
.run => |output| {
const annotated_case_name = b.fmt("run-translated-c {s}", .{case.name});
if (opt_test_filter) |filter| {
if (std.mem.indexOf(u8, annotated_case_name, filter) == null) return;
}
if (!std.process.can_spawn) {
std.debug.print("Unable to spawn child processes on {s}, skipping test.\n", .{@tagName(builtin.os.tag)});
continue; // Pass test.
}
const target_info = std.zig.system.NativeTargetInfo.detect(case.target) catch |err|
std.debug.panic("unable to detect target host: {s}\n", .{@errorName(err)});
if (host.getExternalExecutor(&target_info, .{ .link_libc = true }) != .native) {
// We wouldn't be able to run the compiled C code.
continue; // Pass test.
}
const write_src = b.addWriteFiles();
const file_source = write_src.add("tmp.c", case.input);
const translate_c = b.addTranslateC(.{
.source_file = file_source,
.optimize = .Debug,
.target = case.target,
.link_libc = case.link_libc,
.use_clang = case.c_frontend == .clang,
});
translate_c.step.name = b.fmt("{s} translate-c", .{annotated_case_name});
const run_exe = translate_c.addExecutable(.{});
run_exe.step.name = b.fmt("{s} build-exe", .{annotated_case_name});
run_exe.linkLibC();
const run = b.addRunArtifact(run_exe);
run.step.name = b.fmt("{s} run", .{annotated_case_name});
run.expectStdOutEqual(output);
parent_step.dependOn(&run.step);
},
.translate => |output| {
const annotated_case_name = b.fmt("zig translate-c {s}", .{case.name});
if (opt_test_filter) |filter| {
if (std.mem.indexOf(u8, annotated_case_name, filter) == null) return;
}
const write_src = b.addWriteFiles();
const file_source = write_src.add("tmp.c", case.input);
const translate_c = b.addTranslateC(.{
.source_file = file_source,
.optimize = .Debug,
.target = case.target,
.link_libc = case.link_libc,
.use_clang = case.c_frontend == .clang,
});
translate_c.step.name = annotated_case_name;
const check_file = translate_c.addCheckFile(output);
parent_step.dependOn(&check_file.step);
},
};
}
/// Sort test filenames in-place, so that incremental test cases ("foo.0.zig",
@ -780,7 +899,7 @@ const TestManifestConfigDefaults = struct {
if (std.mem.eql(u8, key, "backend")) {
return "stage2";
} else if (std.mem.eql(u8, key, "target")) {
if (@"type" == .@"error") {
if (@"type" == .@"error" or @"type" == .translate_c or @"type" == .run_translated_c) {
return "native";
}
return comptime blk: {
@ -807,12 +926,16 @@ const TestManifestConfigDefaults = struct {
.@"error" => "Obj",
.run => "Exe",
.compile => "Obj",
.translate_c => "Obj",
.run_translated_c => "Obj",
.cli => @panic("TODO test harness for CLI tests"),
};
} else if (std.mem.eql(u8, key, "is_test")) {
return "0";
return "false";
} else if (std.mem.eql(u8, key, "link_libc")) {
return "0";
return "false";
} else if (std.mem.eql(u8, key, "c_frontend")) {
return "clang";
} else unreachable;
}
};
@ -844,6 +967,8 @@ const TestManifest = struct {
run,
cli,
compile,
translate_c,
run_translated_c,
};
const TrailingIterator = struct {
@ -912,6 +1037,10 @@ const TestManifest = struct {
break :blk .cli;
} else if (std.mem.eql(u8, raw, "compile")) {
break :blk .compile;
} else if (std.mem.eql(u8, raw, "translate-c")) {
break :blk .translate_c;
} else if (std.mem.eql(u8, raw, "run-translated-c")) {
break :blk .run_translated_c;
} else {
std.log.warn("unknown test case type requested: {s}", .{raw});
return error.UnknownTestCaseType;
@ -979,7 +1108,21 @@ const TestManifest = struct {
};
}
fn trailingAlloc(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const []const u8 {
fn trailingSplit(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const u8 {
var out = std.ArrayList(u8).init(allocator);
defer out.deinit();
var trailing_it = self.trailing();
while (trailing_it.next()) |line| {
try out.appendSlice(line);
try out.append('\n');
}
if (out.items.len > 0) {
try out.resize(out.items.len - 1);
}
return try out.toOwnedSlice();
}
fn trailingLines(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const []const u8 {
var out = std.ArrayList([]const u8).init(allocator);
defer out.deinit();
var it = self.trailing();
@ -989,6 +1132,28 @@ const TestManifest = struct {
return try out.toOwnedSlice();
}
fn trailingLinesSplit(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const []const u8 {
// Collect output lines split by empty lines
var out = std.ArrayList([]const u8).init(allocator);
defer out.deinit();
var buf = std.ArrayList(u8).init(allocator);
defer buf.deinit();
var it = self.trailing();
while (it.next()) |line| {
if (line.len == 0) {
if (buf.items.len != 0) {
try out.append(try buf.toOwnedSlice());
buf.items.len = 0;
}
continue;
}
try buf.appendSlice(line);
try buf.append('\n');
}
try out.append(try buf.toOwnedSlice());
return try out.toOwnedSlice();
}
fn ParseFn(comptime T: type) type {
return fn ([]const u8) anyerror!T;
}
@ -1011,8 +1176,10 @@ const TestManifest = struct {
}.parse,
.Bool => return struct {
fn parse(str: []const u8) anyerror!T {
const as_int = try std.fmt.parseInt(u1, str, 0);
return as_int > 0;
if (std.mem.eql(u8, str, "true")) return true;
if (std.mem.eql(u8, str, "false")) return false;
std.debug.print("{s}\n", .{str});
return error.InvalidBool;
}
}.parse,
.Enum => return struct {
@ -1124,9 +1291,47 @@ pub fn main() !void {
if (cases.items.len == 0) {
const backends = try manifest.getConfigForKeyAlloc(arena, "backend", Backend);
const targets = try manifest.getConfigForKeyAlloc(arena, "target", CrossTarget);
const c_frontends = try manifest.getConfigForKeyAlloc(ctx.arena, "c_frontend", CFrontend);
const is_test = try manifest.getConfigForKeyAssertSingle("is_test", bool);
const link_libc = try manifest.getConfigForKeyAssertSingle("link_libc", bool);
const output_mode = try manifest.getConfigForKeyAssertSingle("output_mode", std.builtin.OutputMode);
if (manifest.type == .translate_c) {
for (c_frontends) |c_frontend| {
for (targets) |target| {
const output = try manifest.trailingLinesSplit(ctx.arena);
try ctx.translate.append(.{
.name = std.fs.path.stem(filename),
.c_frontend = c_frontend,
.target = target,
.is_test = is_test,
.link_libc = link_libc,
.input = src,
.kind = .{ .translate = output },
});
}
}
continue;
}
if (manifest.type == .run_translated_c) {
for (c_frontends) |c_frontend| {
for (targets) |target| {
const output = try manifest.trailingSplit(ctx.arena);
try ctx.translate.append(.{
.name = std.fs.path.stem(filename),
.c_frontend = c_frontend,
.target = target,
.is_test = is_test,
.link_libc = link_libc,
.output = output,
.input = src,
.kind = .{ .run = output },
});
}
}
continue;
}
// Cross-product to get all possible test combinations
for (backends) |backend| {
for (targets) |target| {
@ -1158,7 +1363,7 @@ pub fn main() !void {
case.addCompile(src);
},
.@"error" => {
const errors = try manifest.trailingAlloc(arena);
const errors = try manifest.trailingLines(arena);
switch (strategy) {
.independent => {
case.addError(src, errors);
@ -1169,17 +1374,11 @@ pub fn main() !void {
}
},
.run => {
var output = std.ArrayList(u8).init(arena);
var trailing_it = manifest.trailing();
while (trailing_it.next()) |line| {
try output.appendSlice(line);
try output.append('\n');
}
if (output.items.len > 0) {
try output.resize(output.items.len - 1);
}
case.addCompareOutput(src, try output.toOwnedSlice());
const output = try manifest.trailingSplit(ctx.arena);
case.addCompareOutput(src, output);
},
.translate_c => @panic("c_frontend specified for compile case"),
.run_translated_c => @panic("c_frontend specified for compile case"),
.cli => @panic("TODO cli tests"),
}
}
@ -1255,6 +1454,11 @@ fn runCases(self: *Cases, zig_exe_path: []const u8) !void {
host,
);
}
for (self.translate.items) |*case| {
_ = case;
@panic("TODO is this even used?");
}
}
}

View File

@ -3,6 +3,13 @@ const builtin = @import("builtin");
const tests = @import("tests.zig");
const CrossTarget = std.zig.CrossTarget;
// ********************************************************
// * *
// * DO NOT ADD NEW CASES HERE *
// * instead add a file to test/cases/translate_c *
// * *
// ********************************************************
pub fn addCases(cases: *tests.TranslateCContext) void {
const default_enum_type = if (builtin.abi == .msvc) "c_int" else "c_uint";
@ -3315,23 +3322,6 @@ pub fn addCases(cases: *tests.TranslateCContext) void {
\\pub const FOO_CHAR = '\x3f';
});
cases.add("enums",
\\enum Foo {
\\ FooA = 2,
\\ FooB = 5,
\\ Foo1,
\\};
, &[_][]const u8{
\\pub const FooA: c_int = 2;
\\pub const FooB: c_int = 5;
\\pub const Foo1: c_int = 6;
\\pub const enum_Foo =
++ " " ++ default_enum_type ++
\\;
,
\\pub const Foo = enum_Foo;
});
cases.add("macro cast",
\\#include <stdint.h>
\\int baz(void *arg) { return 0; }