Commit bc081901dc

Jakub Konka <kubkon@jakubkonka.com>
2023-10-25 11:40:16
Step.Compile: differentiate between fuzzy and exact matches for compile errors
1 parent 55c7a6d
Changed files (4)
lib
std
test
lib/std/Build/Step/Compile.zig
@@ -204,10 +204,10 @@ use_llvm: ?bool,
 use_lld: ?bool,
 
 /// This is an advanced setting that can change the intent of this Compile step.
-/// If this slice has nonzero length, it means that this Compile step exists to
+/// If this value is non-null, it means that this Compile step exists to
 /// check for compile errors and return *success* if they match, and failure
 /// otherwise.
-expect_errors: []const []const u8 = &.{},
+expect_errors: ?ExpectedCompileErrors = null,
 
 emit_directory: ?*GeneratedFile,
 
@@ -220,6 +220,11 @@ generated_llvm_bc: ?*GeneratedFile,
 generated_llvm_ir: ?*GeneratedFile,
 generated_h: ?*GeneratedFile,
 
+pub const ExpectedCompileErrors = union(enum) {
+    contains: []const u8,
+    exact: []const []const u8,
+};
+
 pub const CSourceFiles = struct {
     dependency: ?*std.Build.Dependency,
     /// If `dependency` is not null relative to it,
@@ -2131,7 +2136,7 @@ fn make(step: *Step, prog_node: *std.Progress.Node) !void {
 
     const maybe_output_bin_path = step.evalZigProcess(zig_args.items, prog_node) catch |err| switch (err) {
         error.NeedCompileErrorCheck => {
-            assert(self.expect_errors.len != 0);
+            assert(self.expect_errors != null);
             try checkCompileErrors(self);
             return;
         },
@@ -2390,52 +2395,70 @@ fn checkCompileErrors(self: *Compile) !void {
 
     // Render the expected lines into a string that we can compare verbatim.
     var expected_generated = std.ArrayList(u8).init(arena);
+    const expect_errors = self.expect_errors.?;
 
     var actual_line_it = mem.splitScalar(u8, actual_stderr, '\n');
-    for (self.expect_errors) |expect_line| {
-        const actual_line = actual_line_it.next() orelse {
-            try expected_generated.appendSlice(expect_line);
-            try expected_generated.append('\n');
-            continue;
-        };
-        if (mem.endsWith(u8, actual_line, expect_line)) {
-            try expected_generated.appendSlice(actual_line);
-            try expected_generated.append('\n');
-            continue;
-        }
-        if (mem.startsWith(u8, expect_line, ":?:?: ")) {
-            if (mem.endsWith(u8, actual_line, expect_line[":?:?: ".len..])) {
-                try expected_generated.appendSlice(actual_line);
-                try expected_generated.append('\n');
-                continue;
+
+    // TODO merge this with the testing.expectEqualStrings logic, and also CheckFile
+    switch (expect_errors) {
+        .contains => |expect_line| {
+            while (actual_line_it.next()) |actual_line| {
+                if (!matchCompileError(actual_line, expect_line)) continue;
+                return;
             }
-        }
-        // We scan for /?/ in expected line and if there is a match, we match everything
-        // up to and after /?/.
-        const expect_line_trim = mem.trim(u8, expect_line, " ");
-        if (mem.indexOf(u8, expect_line_trim, "/?/")) |exp_index| {
-            const actual_line_trim = mem.trim(u8, actual_line, " ");
-            const exp_lhs = expect_line_trim[0..exp_index];
-            const exp_rhs = expect_line_trim[exp_index + "/?/".len ..];
-            if (mem.startsWith(u8, actual_line_trim, exp_lhs) and mem.endsWith(u8, actual_line_trim, exp_rhs)) {
-                try expected_generated.appendSlice(actual_line);
+
+            return self.step.fail(
+                \\
+                \\========= should contain: ===============
+                \\{s}
+                \\========= but not found: ================
+                \\{s}
+                \\=========================================
+            , .{ expect_line, actual_stderr });
+        },
+        .exact => |expect_lines| {
+            for (expect_lines) |expect_line| {
+                const actual_line = actual_line_it.next() orelse {
+                    try expected_generated.appendSlice(expect_line);
+                    try expected_generated.append('\n');
+                    continue;
+                };
+                if (matchCompileError(actual_line, expect_line)) {
+                    try expected_generated.appendSlice(actual_line);
+                    try expected_generated.append('\n');
+                    continue;
+                }
+                try expected_generated.appendSlice(expect_line);
                 try expected_generated.append('\n');
-                continue;
             }
-        }
-        try expected_generated.appendSlice(expect_line);
-        try expected_generated.append('\n');
-    }
 
-    if (mem.eql(u8, expected_generated.items, actual_stderr)) return;
+            if (mem.eql(u8, expected_generated.items, actual_stderr)) return;
 
-    // TODO merge this with the testing.expectEqualStrings logic, and also CheckFile
-    return self.step.fail(
-        \\
-        \\========= expected: =====================
-        \\{s}
-        \\========= but found: ====================
-        \\{s}
-        \\=========================================
-    , .{ expected_generated.items, actual_stderr });
+            return self.step.fail(
+                \\
+                \\========= expected: =====================
+                \\{s}
+                \\========= but found: ====================
+                \\{s}
+                \\=========================================
+            , .{ expected_generated.items, actual_stderr });
+        },
+    }
+}
+
+fn matchCompileError(actual: []const u8, expected: []const u8) bool {
+    if (mem.endsWith(u8, actual, expected)) return true;
+    if (mem.startsWith(u8, expected, ":?:?: ")) {
+        if (mem.endsWith(u8, actual, expected[":?:?: ".len..])) return true;
+    }
+    // We scan for /?/ in expected line and if there is a match, we match everything
+    // up to and after /?/.
+    const expected_trim = mem.trim(u8, expected, " ");
+    if (mem.indexOf(u8, expected_trim, "/?/")) |index| {
+        const actual_trim = mem.trim(u8, actual, " ");
+        const lhs = expected_trim[0..index];
+        const rhs = expected_trim[index + "/?/".len ..];
+        if (mem.startsWith(u8, actual_trim, lhs) and mem.endsWith(u8, actual_trim, rhs)) return true;
+    }
+    return false;
 }
lib/std/Build/Step.zig
@@ -415,7 +415,7 @@ pub fn evalZigProcess(
         .Exited => {
             // Note that the exit code may be 0 in this case due to the
             // compiler server protocol.
-            if (compile.expect_errors.len != 0 and s.result_error_bundle.errorMessageCount() > 0) {
+            if (compile.expect_errors != null and s.result_error_bundle.errorMessageCount() > 0) {
                 return error.NeedCompileErrorCheck;
             }
         },
test/link/elf.zig
@@ -1617,11 +1617,13 @@ fn testLdScriptPathError(b: *Build, opts: Options) *Step {
     exe.addLibraryPath(scripts.getDirectory());
     exe.linkLibC();
 
-    expectLinkErrors(exe, test_step, &.{
-        "error: missing library dependency: GNU ld script '/?/liba.so' requires 'libfoo.so', but file not found",
-        "note: tried libfoo.so",
-        "note: tried /?/libfoo.so",
-    });
+    expectLinkErrors(
+        exe,
+        test_step,
+        .{
+            .contains = "error: missing library dependency: GNU ld script '/?/liba.so' requires 'libfoo.so', but file not found",
+        },
+    );
 
     return test_step;
 }
@@ -1645,10 +1647,10 @@ fn testMismatchedCpuArchitectureError(b: *Build, opts: Options) *Step {
     exe.addObject(obj);
     exe.linkLibC();
 
-    expectLinkErrors(exe, test_step, &.{
+    expectLinkErrors(exe, test_step, .{ .exact = &.{
         "invalid cpu architecture: expected 'x86_64', but found 'aarch64'",
         "note: while parsing /?/a.o",
-    });
+    } });
 
     return test_step;
 }
@@ -2853,12 +2855,12 @@ fn testUnknownFileTypeError(b: *Build, opts: Options) *Step {
     exe.linkLibrary(dylib);
     exe.linkLibC();
 
-    expectLinkErrors(exe, test_step, &.{
+    expectLinkErrors(exe, test_step, .{ .exact = &.{
         "unknown file type",
         "note: while parsing /?/liba.dylib",
         "undefined symbol: foo",
         "note: referenced by /?/a.o:.text",
-    });
+    } });
 
     return test_step;
 }
@@ -2892,11 +2894,11 @@ fn testUnresolvedError(b: *Build, opts: Options) *Step {
     exe.addObject(obj2);
     exe.linkLibC();
 
-    expectLinkErrors(exe, test_step, &.{
+    expectLinkErrors(exe, test_step, .{ .exact = &.{
         "error: undefined symbol: foo",
         "note: referenced by /?/a.o:.text.bar",
         "note: referenced by /?/b.o:.text.main",
-    });
+    } });
 
     return test_step;
 }
@@ -3199,7 +3201,7 @@ fn addAsmSourceBytes(comp: *Compile, bytes: []const u8) void {
     comp.addAssemblyFile(file);
 }
 
-fn expectLinkErrors(comp: *Compile, test_step: *Step, expected_errors: []const []const u8) void {
+fn expectLinkErrors(comp: *Compile, test_step: *Step, expected_errors: Compile.ExpectedCompileErrors) void {
     comp.expect_errors = expected_errors;
     const bin_file = comp.getEmittedBin();
     bin_file.addStepDependencies(test_step);
test/src/Cases.zig
@@ -640,7 +640,7 @@ pub fn lowerToBuildSteps(
             },
             .Error => |expected_msgs| {
                 assert(expected_msgs.len != 0);
-                artifact.expect_errors = expected_msgs;
+                artifact.expect_errors = .{ .exact = expected_msgs };
                 parent_step.dependOn(&artifact.step);
             },
             .Execution => |expected_stdout| no_exec: {