Crafter.Build/tests/Lint/main.cpp

535 lines
25 KiB
C++
Raw Normal View History

2026-07-23 01:24:42 +02:00
// 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;
namespace {
std::int32_t Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++Failures;
}
}
// Scratch-dir fixture for transform tests: Apply mode rewrites files, so
// these cases must never point at the checked-in fixture/. Each case
// writes its own sources into a fresh temp dir.
struct Scratch {
fs::path dir;
explicit Scratch(std::string_view name) {
dir = fs::temp_directory_path() / "crafter-build-lint-format" / name;
fs::remove_all(dir);
fs::create_directories(dir);
}
void Write(std::string_view stem, std::string_view content) const {
std::ofstream f(dir / std::format("{}.cpp", stem), std::ios::binary | std::ios::trunc);
f.write(content.data(), static_cast<std::streamsize>(content.size()));
}
std::string Read(std::string_view stem) const {
std::ifstream f(dir / std::format("{}.cpp", stem), std::ios::binary);
std::stringstream buffer;
buffer << f.rdbuf();
return std::move(buffer).str();
}
Configuration Config(std::vector<fs::path> impls) const {
Configuration cfg;
cfg.path = dir;
cfg.name = "fmt-fixture";
cfg.outputName = "fmt-fixture";
cfg.target = HostTarget();
std::array<fs::path, 0> ifaces = {};
cfg.GetInterfacesAndImplementations(ifaces, impls);
return cfg;
}
};
void AddTrimRule(Configuration& cfg) {
cfg.AddLintRule("trim", [](LintContext& ctx) {
std::string out;
out.reserve(ctx.content.size());
for (std::size_t n = 1; n <= ctx.lines.size(); ++n) {
std::string_view line = ctx.Line(n);
bool crlf = line.ends_with('\r');
if (crlf) line.remove_suffix(1);
while (line.ends_with(' ') || line.ends_with('\t')) line.remove_suffix(1);
out += line;
if (crlf) out += '\r';
if (n < ctx.lines.size() || ctx.content.ends_with('\n')) out += '\n';
}
ctx.SetContent(std::move(out));
});
}
RunLintOptions Mode(LintMode m) {
RunLintOptions opts;
opts.mode = m;
return opts;
}
// Fresh Configuration over the two fixture sources. Rebuilt per case so
// rule registrations don't leak between them.
Configuration FixtureConfig() {
Configuration cfg;
cfg.path = fs::current_path() / "tests" / "Lint" / "fixture";
cfg.name = "lint-fixture";
cfg.outputName = "lint-fixture";
cfg.target = HostTarget();
std::array<fs::path, 0> ifaces = {};
std::array<fs::path, 2> impls = { "clean", "dirty" };
cfg.GetInterfacesAndImplementations(ifaces, impls);
return cfg;
}
void AddTabRule(Configuration& cfg) {
cfg.AddLintRule("tab-rule", [](LintContext& ctx) {
for (std::size_t n = 1; n <= ctx.lines.size(); ++n) {
if (ctx.Line(n).contains('\t')) ctx.Report(n, "tab character");
}
});
}
}
// In-process tests for RunLint: rule registration, file collection over a
// Configuration, glob filtering, --list, comment stripping, and dedup. Lint
// only reads sources, so no Build() is needed.
int main() {
// No rules registered → noRulesDefined, not Clean.
{
Configuration cfg = FixtureConfig();
LintSummary s = RunLint(cfg, {});
Check(s.noRulesDefined, "no rules -> noRulesDefined");
Check(!s.Clean(), "no rules -> not Clean (exit 1)");
Check(s.findings.empty(), "no rules -> no findings");
}
// tab-rule fires on dirty.cpp line 9 only; never-fires stays silent.
{
Configuration cfg = FixtureConfig();
AddTabRule(cfg);
cfg.AddLintRule("never-fires", [](LintContext&) {});
LintSummary s = RunLint(cfg, {});
Check(s.rulesRun == 2, "both rules run");
Check(s.filesLinted == 2, "both fixture files linted");
Check(s.findings.size() == 1, "exactly one finding");
Check(!s.Clean(), "findings -> not Clean");
if (!s.findings.empty()) {
Check(s.findings[0].file.filename() == "dirty.cpp", "finding is in dirty.cpp");
Check(s.findings[0].line == 9, "tab reported at line 9");
Check(s.findings[0].rule == "tab-rule", "finding attributed to tab-rule");
}
}
// Glob filter drops tab-rule → clean run.
{
Configuration cfg = FixtureConfig();
AddTabRule(cfg);
cfg.AddLintRule("never-fires", [](LintContext&) {});
RunLintOptions opts;
opts.globs = { "never-*" };
LintSummary s = RunLint(cfg, opts);
Check(s.rulesRun == 1, "glob filters to one rule");
Check(s.findings.empty(), "filtered run has no findings");
Check(s.Clean(), "filtered run is Clean");
}
// listOnly enumerates without running any rule.
{
Configuration cfg = FixtureConfig();
AddTabRule(cfg);
RunLintOptions opts;
opts.listOnly = true;
LintSummary s = RunLint(cfg, opts);
Check(s.findings.empty(), "listOnly records no findings");
Check(s.filesLinted == 0, "listOnly reads no files");
Check(s.rulesRun == 1, "listOnly still counts matching rules");
}
// CommentStripped: MARKER in a comment (line 7) and in a string literal
// (line 12) are blanked; the identifier at line 13 survives, and line
// numbers computed from the stripped text match the real file.
{
Configuration cfg = FixtureConfig();
cfg.AddLintRule("marker", [](LintContext& ctx) {
const std::string& code = ctx.CommentStripped();
for (std::size_t pos = code.find("MARKER"); pos != std::string::npos;
pos = code.find("MARKER", pos + 1)) {
std::size_t line = 1 + std::count(code.begin(), code.begin() + pos, '\n');
ctx.Report(line, "MARKER in code");
}
});
LintSummary s = RunLint(cfg, {});
Check(s.findings.size() == 1, "only the code MARKER is found");
if (!s.findings.empty()) {
Check(s.findings[0].line == 13, "code MARKER reported at line 13");
Check(s.findings[0].file.filename() == "dirty.cpp", "MARKER finding is in dirty.cpp");
}
}
// Duplicate rule name: first registration wins, second never runs.
{
Configuration cfg = FixtureConfig();
AddTabRule(cfg);
cfg.AddLintRule("tab-rule", [](LintContext& ctx) {
ctx.Report(1, "duplicate ran");
});
LintSummary s = RunLint(cfg, {});
Check(s.rulesRun == 1, "duplicate name deduplicated");
Check(s.findings.size() == 1 && s.findings[0].line == 9, "only the first registration ran");
}
// A throwing rule surfaces as a finding, not an unwind.
{
Configuration cfg = FixtureConfig();
cfg.AddLintRule("throws", [](LintContext& ctx) {
if (ctx.file.filename() == "clean.cpp") throw std::runtime_error("boom");
});
LintSummary s = RunLint(cfg, {});
Check(s.findings.size() == 1, "exception becomes a finding");
if (!s.findings.empty()) {
Check(s.findings[0].message.contains("boom"), "finding carries the exception message");
Check(s.findings[0].line == 0, "exception finding is whole-file (line 0)");
}
}
// --- Transform (format) cases: scratch dir, never the checked-in fixture ---
// Report mode: transform diffs become would-reformat findings; disk untouched.
{
Scratch s("report");
s.Write("a", "hello \nworld\n");
Configuration cfg = s.Config({"a"});
AddTrimRule(cfg);
LintSummary sum = RunLint(cfg, Mode(LintMode::Report));
Check(sum.findings.size() == 1, "one would-reformat finding");
if (!sum.findings.empty()) {
Check(sum.findings[0].line == 1, "would-reformat at line 1");
Check(sum.findings[0].rule == "trim", "attributed to the transform rule");
Check(sum.findings[0].message == "would reformat", "derived message");
}
Check(sum.changedFiles.size() == 1, "changedFiles populated in Report mode");
Check(!sum.Clean(), "would-reformat gates lint");
Check(s.Read("a") == "hello \nworld\n", "Report mode never writes");
}
// Apply mode: disk rewritten; a sibling report-only rule's findings are
// still recorded (the verb, not the driver, ignores them).
{
Scratch s("apply");
s.Write("a", "hello \nworld\n");
Configuration cfg = s.Config({"a"});
AddTrimRule(cfg);
cfg.AddLintRule("note", [](LintContext& ctx) { ctx.Report(2, "note"); });
LintSummary sum = RunLint(cfg, Mode(LintMode::Apply));
Check(s.Read("a") == "hello\nworld\n", "Apply rewrites the file");
Check(sum.changedFiles.size() == 1, "one file formatted");
bool hasNote = std::any_of(sum.findings.begin(), sum.findings.end(), [](const LintFinding& f) { return f.rule == "note"; });
Check(hasNote, "report-only findings still recorded in Apply mode");
Check(sum.errors == 0, "clean apply has no errors");
}
// Check mode: reported, not written.
{
Scratch s("check");
s.Write("a", "hello \n");
Configuration cfg = s.Config({"a"});
AddTrimRule(cfg);
LintSummary sum = RunLint(cfg, Mode(LintMode::Check));
Check(sum.changedFiles.size() == 1, "Check records would-change file");
Check(!sum.findings.empty(), "Check records would-reformat findings");
Check(s.Read("a") == "hello \n", "Check mode never writes");
}
// Chaining: rule2 sees rule1's output; both attributed in Report mode.
{
Scratch s("chain");
s.Write("a", "AAA\n");
Configuration cfg = s.Config({"a"});
cfg.AddLintRule("one", [](LintContext& ctx) {
std::string c = ctx.content;
if (auto p = c.find("AAA"); p != std::string::npos) c.replace(p, 3, "BBB");
ctx.SetContent(std::move(c));
});
cfg.AddLintRule("two", [](LintContext& ctx) {
std::string c = ctx.content;
if (auto p = c.find("BBB"); p != std::string::npos) c.replace(p, 3, "CCC");
ctx.SetContent(std::move(c));
});
LintSummary rep = RunLint(cfg, Mode(LintMode::Report));
bool one = std::any_of(rep.findings.begin(), rep.findings.end(), [](const LintFinding& f) { return f.rule == "one"; });
bool two = std::any_of(rep.findings.begin(), rep.findings.end(), [](const LintFinding& f) { return f.rule == "two"; });
Check(one && two, "chained transforms both attributed");
Check(s.Read("a") == "AAA\n", "Report leaves chain input untouched");
LintSummary app = RunLint(cfg, Mode(LintMode::Apply));
Check(s.Read("a") == "CCC\n", "Apply composes chained transforms");
Check(app.changedFiles.size() == 1, "chain counts as one changed file");
}
// A throwing transform is reverted — half-applied content never lands.
{
Scratch s("throws");
s.Write("a", "keep\n");
Configuration cfg = s.Config({"a"});
cfg.AddLintRule("bad", [](LintContext& ctx) {
ctx.SetContent("garbage");
throw std::runtime_error("mid-transform");
});
LintSummary sum = RunLint(cfg, Mode(LintMode::Apply));
Check(s.Read("a") == "keep\n", "throwing transform reverted, disk untouched");
Check(sum.errors == 1, "exception counted as error");
Check(sum.changedFiles.empty(), "reverted transform is not a change");
bool threw = std::any_of(sum.findings.begin(), sum.findings.end(),
[](const LintFinding& f) {
return f.line == 0 && f.message.contains("mid-transform");
});
Check(threw, "exception surfaced as line-0 finding");
}
// CRLF byte fidelity + the final-newline whole-file (line 0) diff edge.
{
Scratch s("bytes");
s.Write("crlf", "x \r\ny\r\n");
s.Write("noeol", "a\nb");
Configuration cfg = s.Config({"crlf", "noeol"});
AddTrimRule(cfg);
cfg.AddLintRule("final-newline", [](LintContext& ctx) {
if (!ctx.content.empty() && !ctx.content.ends_with('\n')) {
ctx.SetContent(ctx.content + '\n');
}
});
LintSummary rep = RunLint(cfg, Mode(LintMode::Report));
bool wholeFile = std::any_of(rep.findings.begin(), rep.findings.end(),
[](const LintFinding& f) {
return f.rule == "final-newline" && f.line == 0;
});
Check(wholeFile, "missing final newline reports as whole-file finding");
RunLint(cfg, Mode(LintMode::Apply));
Check(s.Read("crlf") == "x\r\ny\r\n", "trim preserves CRLF endings");
Check(s.Read("noeol") == "a\nb\n", "final-newline appends exactly one newline");
}
// Mixed rule: Report() and SetContent() from the same rule.
{
Scratch s("mixed");
s.Write("a", "bad \n");
Configuration cfg = s.Config({"a"});
cfg.AddLintRule("mixed", [](LintContext& ctx) {
ctx.Report(1, "flagged");
std::string c = ctx.content;
if (auto p = c.find(' '); p != std::string::npos) c.erase(p, 1);
ctx.SetContent(std::move(c));
});
LintSummary rep = RunLint(cfg, Mode(LintMode::Report));
bool flagged = std::any_of(rep.findings.begin(), rep.findings.end(), [](const LintFinding& f) { return f.message == "flagged"; });
bool reformat = std::any_of(rep.findings.begin(), rep.findings.end(), [](const LintFinding& f) { return f.message == "would reformat"; });
Check(flagged && reformat, "mixed rule records both finding kinds");
RunLint(cfg, Mode(LintMode::Apply));
Check(s.Read("a") == "bad\n", "mixed rule's transform applied");
}
// Idempotency: a second Apply is a no-op; identical SetContent bytes are
// not a change (compare-by-value, not call-tracking).
{
Scratch s("idempotent");
s.Write("a", "hello \n");
Configuration cfg = s.Config({"a"});
AddTrimRule(cfg);
LintSummary first = RunLint(cfg, Mode(LintMode::Apply));
Check(first.changedFiles.size() == 1, "first apply changes the file");
LintSummary second = RunLint(cfg, Mode(LintMode::Apply));
Check(second.changedFiles.empty(), "second apply is a no-op");
Check(second.findings.empty(), "no-op apply has no findings");
}
// --- Suppression directives (engine-level) ---
// next-line directive with a rule name suppresses that finding only.
{
Scratch s("suppress-next-line");
s.Write("a", "// lint-disable-next-line flag\nbad\nbad\n");
Configuration cfg = s.Config({"a"});
cfg.AddLintRule("flag", [](LintContext& ctx) {
for (std::size_t n = 1; n <= ctx.lines.size(); ++n) {
if (ctx.Line(n).contains("bad")) ctx.Report(n, "bad");
}
});
LintSummary sum = RunLint(cfg, Mode(LintMode::Report));
Check(sum.findings.size() == 1, "next-line directive suppresses one finding");
if (!sum.findings.empty()) Check(sum.findings[0].line == 3, "the unsuppressed line still reports");
}
// next-line suppression also reverts a transform's edit on that line —
// `format` must not rewrite what lint is told to ignore.
{
Scratch s("suppress-transform");
s.Write("a", "// lint-disable-next-line trim\nkeep \ntrim \n");
Configuration cfg = s.Config({"a"});
AddTrimRule(cfg);
RunLint(cfg, Mode(LintMode::Apply));
Check(s.Read("a") == "// lint-disable-next-line trim\nkeep \ntrim\n",
"suppressed line keeps its bytes; the unsuppressed one is fixed");
}
// Multiple rule names on one directive (space- or comma-separated).
{
Scratch s("suppress-multi");
s.Write("a", "// lint-disable-next-line flagA, flagB\nbad \nbad \n");
Configuration cfg = s.Config({"a"});
cfg.AddLintRule("flagA", [](LintContext& ctx) {
for (std::size_t n = 1; n <= ctx.lines.size(); ++n) {
if (ctx.Line(n).contains("bad")) ctx.Report(n, "A");
}
});
cfg.AddLintRule("flagB", [](LintContext& ctx) {
for (std::size_t n = 1; n <= ctx.lines.size(); ++n) {
if (ctx.Line(n).contains("bad")) ctx.Report(n, "B");
}
});
AddTrimRule(cfg);
LintSummary sum = RunLint(cfg, Mode(LintMode::Report));
bool line2Silent = std::none_of(sum.findings.begin(), sum.findings.end(), [](const LintFinding& f) { return f.line == 2 && f.rule != "trim"; });
bool line3Loud = std::count_if(sum.findings.begin(), sum.findings.end(), [](const LintFinding& f) { return f.line == 3; }) >= 2;
Check(line2Silent, "both named rules suppressed on the target line");
bool trimStillFires = std::any_of(sum.findings.begin(), sum.findings.end(), [](const LintFinding& f) { return f.line == 2 && f.rule == "trim"; });
Check(trimStillFires, "unnamed rule still fires on the target line");
Check(line3Loud, "unsuppressed line reports from both rules");
}
// file-level all-rules directive silences findings and stops format.
{
Scratch s("suppress-file");
s.Write("a", "// lint-disable-file\nbad \n");
Configuration cfg = s.Config({"a"});
AddTrimRule(cfg);
cfg.AddLintRule("flag", [](LintContext& ctx) { ctx.Report(2, "bad"); });
LintSummary rep = RunLint(cfg, Mode(LintMode::Report));
Check(rep.findings.empty(), "file-level all directive suppresses every finding");
LintSummary app = RunLint(cfg, Mode(LintMode::Apply));
Check(app.changedFiles.empty(), "file-level all directive stops format");
Check(s.Read("a") == "// lint-disable-file\nbad \n", "file bytes untouched");
}
// file-level with a rule name: that rule is dead, others still act.
{
Scratch s("suppress-file-rule");
s.Write("a", "// lint-disable-file flag\nbad \n");
Configuration cfg = s.Config({"a"});
AddTrimRule(cfg);
cfg.AddLintRule("flag", [](LintContext& ctx) { ctx.Report(2, "bad"); });
LintSummary rep = RunLint(cfg, Mode(LintMode::Report));
bool onlyTrim = !rep.findings.empty() && std::all_of(rep.findings.begin(), rep.findings.end(), [](const LintFinding& f) { return f.rule == "trim"; });
Check(onlyTrim, "file-level rule directive kills that rule, trim still fires");
RunLint(cfg, Mode(LintMode::Apply));
Check(s.Read("a") == "// lint-disable-file flag\nbad\n", "other rules still format");
}
feat(lint): libclang-backed token layer Adds LintContext::Tokens() and friends, backed by clang_tokenize, as the substrate the rules will move onto. Nothing consumes it yet. libclang is dlopen'd rather than linked: -lclang would break the mingw and MSVC cross-builds at link time and would put a libclang.so.NN runtime dependency into the otherwise self-contained release tarballs. The clang-c header is used for its declarations only, and the function-pointer table is typed with decltype so the signatures cannot drift from the real API. Three properties this buys that the hand-rolled scanners could not have: - a raw string literal or block comment is ONE token, so the documented "raw string literals are not recognized" limitation goes away; - `//` inside a literal is not a comment, so LineHasComment() replaces the Line(n).contains("//") probes that false-positive on it; - tokens cover preprocessor branches that are inactive for the host, since clang_tokenize lexes rather than evaluates #if. Token rules therefore keep seeing every platform's code, which an AST could not offer. The parse backing the tokenizer is expected to fail on module units — no PCMs, no build flags — and that is fine, because lexing has no semantic prerequisites. Verified in the new tests. LintSummary::Clean() now counts `errors`. It previously ignored them, so an infrastructure failure that produced no findings reported clean and exited 0; a missing libclang would have been exactly that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 02:54:38 +02:00
// ---------------- token layer ----------------
//
// The source is spelled with escaped literals rather than a raw string so
// that this file stays lintable by the very rules under test; the scratch
// file it writes does contain a genuine multi-line raw string.
{
constexpr std::string_view Source =
"#ifdef CRAFTER_LINT_NEVER_DEFINED\n" // 1
"void HiddenBranch();\n" // 2
"#endif\n" // 3
"// a real comment\n" // 4
"int url = 1; // https://example.com\n" // 5
"auto raw = R\"raw(spans lines\n" // 6
" // not a comment\n" // 7
" int notADecl;\n" // 8
")raw\";\n"; // 9
Scratch s("tokens");
s.Write("f", Source);
Configuration cfg = s.Config({"f"});
cfg.AddLintRule("tokens", [](LintContext& ctx) {
std::span<const LintToken> toks = ctx.Tokens();
Check(!toks.empty(), "tokens: file lexes to a non-empty stream");
// Every token's offset/length must address its own bytes, or a
// transform editing at an offset would corrupt the file.
bool offsetsSound = true;
for (const LintToken& t : toks) {
if (t.offset + t.length > ctx.content.size() || ctx.TokenText(t).empty()) offsetsSound = false;
}
Check(offsetsSound, "tokens: every offset/length addresses real bytes");
// Ordered by offset, so binary search in TokensOnLine is valid.
bool ordered = std::ranges::is_sorted(toks, {}, &LintToken::offset);
Check(ordered, "tokens: stream is in source order");
// Inactive #ifdef branch is still lexed — this is what keeps token
// rules covering every platform, unlike an AST.
bool sawHidden = std::ranges::any_of(toks, [&](const LintToken& t) {
return t.kind == LintTokenKind::Identifier && ctx.TokenText(t) == "HiddenBranch";
});
Check(sawHidden, "tokens: inactive #ifdef branch is lexed");
// A multi-line raw string is exactly one Literal, comment markers
// and declarations inside it included.
auto isRaw = [&](const LintToken& t) { return ctx.TokenText(t).starts_with("R\"raw("); };
Check(std::ranges::count_if(toks, isRaw) == 1, "tokens: raw string is a single token");
auto raw = std::ranges::find_if(toks, isRaw);
if (raw != toks.end()) {
Check(raw->kind == LintTokenKind::Literal, "tokens: raw string is a Literal");
Check(raw->line == 6, "tokens: raw string starts on line 6");
Check(ctx.TokenText(*raw).contains("// not a comment"), "tokens: raw string body kept intact");
Check(ctx.TokenText(*raw).ends_with(")raw\""), "tokens: raw string spans to its own terminator");
}
// The only comments are the two real ones on lines 4 and 5 — the
// `//` on line 7 lives inside the raw string.
std::vector<std::size_t> commentLines;
for (const LintToken& t : toks) {
if (t.kind == LintTokenKind::Comment) commentLines.push_back(t.line);
}
Check(commentLines == std::vector<std::size_t>{4, 5}, "tokens: only real comments are Comment tokens");
Check(ctx.LineHasComment(4), "tokens: LineHasComment finds a whole-line comment");
Check(ctx.LineHasComment(5), "tokens: LineHasComment finds a trailing comment");
Check(!ctx.LineHasComment(7), "tokens: `//` inside a raw string is not a comment");
Check(!ctx.LineHasComment(2), "tokens: code-only line has no comment");
// TokensOnLine brackets by starting line.
std::span<const LintToken> line2 = ctx.TokensOnLine(2);
Check(!line2.empty() && ctx.TokenText(line2.front()) == "void", "tokens: TokensOnLine starts at the line's first token");
Check(std::ranges::all_of(line2, [](const LintToken& t) { return t.line == 2; }), "tokens: TokensOnLine stays on its line");
// SetContent must invalidate the cache, or offsets point into a
// buffer that no longer exists.
ctx.SetContent("int replaced;\n");
std::span<const LintToken> after = ctx.Tokens();
Check(!after.empty() && ctx.TokenText(after.front()) == "int", "tokens: re-lexed after SetContent");
Check(std::ranges::none_of(after, [&](const LintToken& t) { return ctx.TokenText(t) == "HiddenBranch"; }),
"tokens: stale tokens are dropped after SetContent");
});
RunLint(cfg, Mode(LintMode::Report));
}
// Non-C++ extensions are not lexed: GLSL through a C++ lexer would produce
// plausible-looking nonsense rather than an honest refusal.
{
Scratch s("tokens-foreign");
fs::path shader = s.dir / "f.frag";
{
std::ofstream f(shader, std::ios::binary | std::ios::trunc);
f << "#version 450\nvoid main() { }\n";
}
Configuration cfg = s.Config({});
cfg.shaders.emplace_back(fs::path(shader), "main", ShaderType::Fragment);
cfg.AddLintRule("no-lex", [](LintContext& ctx) {
Check(ctx.Tokens().empty(), "tokens: shaders are not lexed as C++");
});
RunLint(cfg, Mode(LintMode::Report));
}
2026-07-23 01:24:42 +02:00
if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures);
return 1;
}
return 0;
}