test: a warning on any compile path must not fail the build
The fixture puts a #warning in each kind of source the build compiles — module interface, module implementation, C, consumer — plus an unknown -z value so ld.lld warns on the link step too, and asserts the build succeeds cold (every unit compiling, every warning emitted), succeeds with nothing to do, and succeeds again after each source is touched back into staleness. The staged copy lives outside the repo because the whole test depends on units compiling for the first time. The last pass breaks a source on purpose: "never fails" would satisfy everything above just as well, so the error path is asserted too — both that it fails and that it still reports the compiler's diagnostic.
This commit is contained in:
parent
8e2d21a2bc
commit
67d31689bc
6 changed files with 211 additions and 0 deletions
|
|
@ -110,6 +110,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.AddTest("IncrementalInterfaceChange").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("IncrementalInterfaceChange").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("TransitiveInterfaceChange").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("TransitiveInterfaceChange").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("IncrementalHeaderChange").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("IncrementalHeaderChange").Dependencies({ CrafterBuildLib.get() });
|
||||||
|
cfg.AddTest("CompilerWarning").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("CleanProject").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("CleanProject").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("ShaderCompile").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("ShaderCompile").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("StandardArgs").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("StandardArgs").Dependencies({ CrafterBuildLib.get() });
|
||||||
|
|
|
||||||
9
tests/CompilerWarning/fixture/lib/Widget.cpp
Normal file
9
tests/CompilerWarning/fixture/lib/Widget.cpp
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
module;
|
||||||
|
#warning "implementation unit is noisy"
|
||||||
|
module Widget;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
std::int32_t WidgetValue() { return 42; }
|
||||||
13
tests/CompilerWarning/fixture/lib/Widget.cppm
Normal file
13
tests/CompilerWarning/fixture/lib/Widget.cppm
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
module;
|
||||||
|
// Every source in this fixture carries a #warning: -W#warnings is on by
|
||||||
|
// default, so each unit produces diagnostics on stderr and still exits 0.
|
||||||
|
// That is the whole point — the build has to read the exit status, not the
|
||||||
|
// chatter.
|
||||||
|
#warning "interface unit is noisy"
|
||||||
|
export module Widget;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
export std::int32_t WidgetValue();
|
||||||
8
tests/CompilerWarning/fixture/lib/counter.c
Normal file
8
tests/CompilerWarning/fixture/lib/counter.c
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
/* SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® */
|
||||||
|
|
||||||
|
#warning "C source is noisy"
|
||||||
|
|
||||||
|
long CounterValue(void) {
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
17
tests/CompilerWarning/fixture/main.cpp
Normal file
17
tests/CompilerWarning/fixture/main.cpp
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
#warning "consumer is noisy"
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import Widget;
|
||||||
|
|
||||||
|
extern "C" long CounterValue();
|
||||||
|
|
||||||
|
// Prints "<module value> <C value>". The test asserts on it so that "the build
|
||||||
|
// succeeded" also means the objects behind those two numbers were really
|
||||||
|
// produced, not merely left over from an earlier pass.
|
||||||
|
int main() {
|
||||||
|
std::print("{} {}", WidgetValue(), CounterValue());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
163
tests/CompilerWarning/main.cpp
Normal file
163
tests/CompilerWarning/main.cpp
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import Crafter.Build;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
// A compiler warning must not fail the build. Success used to be inferred from
|
||||||
|
// whether the command printed anything, and a warning is printing something —
|
||||||
|
// so a translation unit that warns failed the build the moment it was compiled
|
||||||
|
// and passed on every run after that, because warnings are only emitted when
|
||||||
|
// something actually recompiles. Same source, opposite verdicts, decided by
|
||||||
|
// whether an object file happened to be up to date: it read as a flaky test,
|
||||||
|
// and a cold CI checkout turned every latent warning in a project into a wall
|
||||||
|
// of unrelated failures.
|
||||||
|
//
|
||||||
|
// The fixture puts a #warning in each kind of source the build knows how to
|
||||||
|
// compile — module interface, module implementation, C, consumer — and a
|
||||||
|
// linker warning on top, then asserts the build succeeds cold, succeeds when
|
||||||
|
// there is nothing to do, and succeeds again after each source is touched back
|
||||||
|
// into staleness. The last pass introduces a real error, because "never fails"
|
||||||
|
// would satisfy everything above just as well.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::int32_t Failures = 0;
|
||||||
|
|
||||||
|
void Check(bool cond, std::string_view msg) {
|
||||||
|
if (!cond) {
|
||||||
|
std::println(std::cerr, "FAIL: {}", msg);
|
||||||
|
++Failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The fixture is mutated during the run, and every assertion here depends
|
||||||
|
// on units compiling for the first time, so work on a fresh copy outside
|
||||||
|
// the repo rather than against whatever an earlier run left behind.
|
||||||
|
fs::path StageFixture() {
|
||||||
|
fs::path source = fs::current_path() / "tests" / "CompilerWarning" / "fixture";
|
||||||
|
fs::path staged = fs::temp_directory_path() / "crafter-build-compiler-warning";
|
||||||
|
fs::remove_all(staged);
|
||||||
|
fs::copy(source, staged, fs::copy_options::recursive);
|
||||||
|
return staged;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Configuration> MakeLib(const fs::path& staged) {
|
||||||
|
auto lib = std::make_unique<Configuration>();
|
||||||
|
lib->path = staged / "lib";
|
||||||
|
lib->name = "noisy-widget";
|
||||||
|
lib->outputName = "noisy-widget";
|
||||||
|
lib->target = HostTarget();
|
||||||
|
lib->type = ConfigurationType::LibraryStatic;
|
||||||
|
std::array<fs::path, 1> ifaces = { "Widget" };
|
||||||
|
std::array<fs::path, 1> impls = { "Widget" };
|
||||||
|
lib->GetInterfacesAndImplementations(ifaces, impls);
|
||||||
|
// cFiles are resolved against the cwd at build time, so spell it out.
|
||||||
|
lib->cFiles = { staged / "lib" / "counter" };
|
||||||
|
return lib;
|
||||||
|
}
|
||||||
|
|
||||||
|
Configuration MakeApp(const fs::path& staged, Configuration* lib) {
|
||||||
|
Configuration app;
|
||||||
|
app.path = staged;
|
||||||
|
app.name = "noisy-app";
|
||||||
|
app.outputName = "noisy-app";
|
||||||
|
app.target = HostTarget();
|
||||||
|
app.type = ConfigurationType::Executable;
|
||||||
|
std::array<fs::path, 0> ifaces = {};
|
||||||
|
std::array<fs::path, 1> impls = { "main" };
|
||||||
|
app.GetInterfacesAndImplementations(ifaces, impls);
|
||||||
|
app.dependencies = { lib };
|
||||||
|
// The link step reads a command's output the same way a compile does,
|
||||||
|
// so cover it too: an unknown -z value makes ld.lld warn and exit 0.
|
||||||
|
// GNU-style only — lld-link spells its options differently — so the
|
||||||
|
// link case rides along on hosts that use it and is skipped elsewhere.
|
||||||
|
if (!app.target.contains("windows")) {
|
||||||
|
app.linkFlags = { "-Wl,-z,crafter-build-nonexistent-z-value" };
|
||||||
|
}
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildResult BuildOnce(Configuration& app) {
|
||||||
|
// A fresh depResults per pass: the map memoizes each Configuration's
|
||||||
|
// build for the duration of one pass, so reusing it would skip the
|
||||||
|
// library's rebuild entirely.
|
||||||
|
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
||||||
|
std::mutex depMutex;
|
||||||
|
return Build(app, depResults, depMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BuildOk(Configuration& app, std::string_view label) {
|
||||||
|
BuildResult r = BuildOnce(app);
|
||||||
|
if (!r.result.empty()) {
|
||||||
|
std::println(std::cerr, "FAIL: {} build failed: {}", label, r.result);
|
||||||
|
++Failures;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheckRun(const fs::path& binary, std::string_view label) {
|
||||||
|
auto r = RunCommandWithTimeout(binary.string(), std::chrono::seconds(30));
|
||||||
|
Check(r.exitCode == 0 && !r.crashed && !r.timedOut && r.output == "42 7", std::format("{}: expected '42 7', got '{}' (exit={})", label, r.output, r.exitCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Touch(const fs::path& source) {
|
||||||
|
fs::last_write_time(source, fs::file_time_type::clock::now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
fs::path staged = StageFixture();
|
||||||
|
std::unique_ptr<Configuration> lib = MakeLib(staged);
|
||||||
|
Configuration app = MakeApp(staged, lib.get());
|
||||||
|
fs::path binary = app.BinDir() / "noisy-app";
|
||||||
|
|
||||||
|
// Cold: every unit compiles, so every #warning in the fixture is emitted
|
||||||
|
// and the linker warns as well. This is the pass that used to fail.
|
||||||
|
if (!BuildOk(app, "cold")) {
|
||||||
|
std::println(std::cerr, "{} assertions failed", Failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
CheckRun(binary, "cold");
|
||||||
|
|
||||||
|
// Nothing to recompile, so nothing warns. Same sources as the pass above,
|
||||||
|
// and the verdict has to match it.
|
||||||
|
if (BuildOk(app, "idle")) {
|
||||||
|
CheckRun(binary, "idle");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back into staleness one source at a time: whichever compile path a
|
||||||
|
// warning comes out of, it stays non-fatal.
|
||||||
|
for (auto [source, label] : std::initializer_list<std::pair<fs::path, std::string_view>>{
|
||||||
|
{ staged / "lib" / "Widget.cppm", "interface unit" },
|
||||||
|
{ staged / "lib" / "Widget.cpp", "implementation unit" },
|
||||||
|
{ staged / "lib" / "counter.c", "C source" },
|
||||||
|
{ staged / "main.cpp", "consumer" },
|
||||||
|
}) {
|
||||||
|
Touch(source);
|
||||||
|
if (BuildOk(app, label)) {
|
||||||
|
CheckRun(binary, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other direction, last because it leaves the fixture broken: a real
|
||||||
|
// error still has to fail, and still has to say what went wrong.
|
||||||
|
{
|
||||||
|
std::ofstream broken(staged / "main.cpp", std::ios::binary | std::ios::trunc);
|
||||||
|
broken << "int main() { return NotDeclaredAnywhere(); }\n";
|
||||||
|
broken.close();
|
||||||
|
Touch(staged / "main.cpp");
|
||||||
|
|
||||||
|
BuildResult r = BuildOnce(app);
|
||||||
|
Check(!r.result.empty(), "a source that fails to compile fails the build");
|
||||||
|
Check(r.result.contains("error:"), std::format("the failure reports the compiler's diagnostic, got '{}'", r.result));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Failures > 0) {
|
||||||
|
std::println(std::cerr, "{} assertions failed", Failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue