linting
Some checks failed
CI / build-test-release (push) Failing after 7m15s

This commit is contained in:
Jorijn van der Graaf 2026-07-23 01:24:42 +02:00
commit 8892154b28
70 changed files with 2780 additions and 596 deletions

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
#include <stdlib.h>
import std;
@ -27,7 +27,7 @@ namespace {
if (const char* env = std::getenv("CRAFTER_BUILD_HOME"); env && *env) {
return env;
}
for (const char* candidate : {
for (std::string_view candidate : {
"/usr/local/share/crafter-build",
"/usr/share/crafter-build",
}) {
@ -47,8 +47,7 @@ namespace {
int main() {
fs::path home = FindCrafterBuildHome();
if (home.empty()) {
std::println(std::cerr,
"SKIP: no installed share/crafter-build found and CRAFTER_BUILD_HOME unset");
std::println(std::cerr, "SKIP: no installed share/crafter-build found and CRAFTER_BUILD_HOME unset");
return 77;
}
setenv("CRAFTER_BUILD_HOME", home.string().c_str(), 1);
@ -64,9 +63,9 @@ int main() {
fs::create_directories(cacheDir);
setenv("XDG_CACHE_HOME", cacheDir.string().c_str(), 1);
constexpr int N = 4;
constexpr std::int32_t N = 4;
std::vector<fs::path> projects;
for (int i = 0; i < N; ++i) {
for (std::int32_t i = 0; i < N; ++i) {
fs::path dir = scratch / std::format("p{}", i);
fs::create_directories(dir);
WriteFile(dir / "project.cpp", std::format(
@ -89,7 +88,7 @@ int main() {
std::array<std::string, N> errors;
std::vector<std::thread> threads;
threads.reserve(N);
for (int i = 0; i < N; ++i) {
for (std::int32_t i = 0; i < N; ++i) {
threads.emplace_back([&, i]() {
try {
std::array<std::string_view, 0> args = {};
@ -101,8 +100,8 @@ int main() {
}
for (std::thread& t : threads) t.join();
int failures = 0;
for (int i = 0; i < N; ++i) {
std::int32_t failures = 0;
for (std::int32_t i = 0; i < N; ++i) {
if (!errors[i].empty()) {
std::println(std::cerr, "FAIL p{}: {}", i, errors[i]);
++failures;
@ -112,9 +111,7 @@ int main() {
fs::remove_all(scratch, ec);
if (failures > 0) {
std::println(std::cerr,
"{}/{} concurrent LoadProject() invocations failed (host-cache race)",
failures, N);
std::println(std::cerr, "{}/{} concurrent LoadProject() invocations failed (host-cache race)", failures, N);
return 1;
}
return 0;

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import DepMod;

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module DepMod;

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module DepMod;
export int dep_value();

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
@ -101,10 +101,7 @@ int main() {
// reset the (cached, never-rebuilt) dependency's module flag. Before the
// fix this was false and an intra-config waiter would have deadlocked.
if (!dep.interfaces[0]->compiled.load()) {
std::println(std::cerr,
"FAIL: consumer build reset a cached dependency's module 'compiled' "
"flag (issue #16 regression) — this is the state that deadlocks a "
"concurrent build");
std::println(std::cerr, "FAIL: consumer build reset a cached dependency's module 'compiled' " "flag (issue #16 regression) — this is the state that deadlocks a " "concurrent build");
return 1;
}
@ -115,8 +112,7 @@ int main() {
}
auto run = RunCommandWithTimeout(bin.string(), std::chrono::seconds(10));
if (run.exitCode != 0 || run.timedOut || run.crashed) {
std::println(std::cerr, "consumer exe did not exit cleanly: exit={} output={}",
run.exitCode, run.output);
std::println(std::cerr, "consumer exe did not exit cleanly: exit={} output={}", run.exitCode, run.output);
return 1;
}
if (run.output != "42") {

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Calc;

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module Calc;
import std;

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
@ -44,18 +44,15 @@ int main() {
}
// The import resolves to the dependency's interface, not a local one.
if (!app.implementations[0].moduleDependencies.empty()) {
std::println(std::cerr, "expected no local module deps, got {}",
app.implementations[0].moduleDependencies.size());
std::println(std::cerr, "expected no local module deps, got {}", app.implementations[0].moduleDependencies.size());
return 1;
}
if (app.implementations[0].externalModuleDependencies.size() != 1) {
std::println(std::cerr, "expected 1 external module dep, got {}",
app.implementations[0].externalModuleDependencies.size());
std::println(std::cerr, "expected 1 external module dep, got {}", app.implementations[0].externalModuleDependencies.size());
return 1;
}
if (app.implementations[0].externalModuleDependencies[0].first->name != "Calc") {
std::println(std::cerr, "expected external dep 'Calc', got '{}'",
app.implementations[0].externalModuleDependencies[0].first->name);
std::println(std::cerr, "expected external dep 'Calc', got '{}'", app.implementations[0].externalModuleDependencies[0].first->name);
return 1;
}
@ -81,8 +78,7 @@ int main() {
auto run = RunCommandWithTimeout(bin.string(), std::chrono::seconds(10));
if (run.exitCode != 0 || run.timedOut || run.crashed) {
std::println(std::cerr, "exe did not exit cleanly: exit={} output={}",
run.exitCode, run.output);
std::println(std::cerr, "exe did not exit cleanly: exit={} output={}", run.exitCode, run.output);
return 1;
}
if (run.output != "7") {

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
int main() { return 0; }

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;

260
tests/HouseRules/main.cpp Normal file
View file

@ -0,0 +1,260 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
#include "../../lint-rules.h"
namespace fs = std::filesystem;
using namespace Crafter;
// Validates the house-style ruleset in lint-rules.h — the transforms and
// reports this repo (and sibling repos that copy the header) rely on. Each
// case writes a scratch file, runs ONE rule via the glob filter, and asserts
// the findings and/or rewritten bytes.
namespace {
std::int32_t Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++Failures;
}
}
struct RuleRun {
LintSummary summary;
std::string text; // file content after the run
};
RuleRun RunRule(std::string_view content, std::string_view rule, LintMode mode) {
static std::int32_t Counter = 0;
fs::path dir = fs::temp_directory_path() / "crafter-build-house-rules" / std::format("case-{}", Counter++);
fs::remove_all(dir);
fs::create_directories(dir);
{
std::ofstream f(dir / "f.cpp", std::ios::binary | std::ios::trunc);
f.write(content.data(), static_cast<std::streamsize>(content.size()));
}
Configuration cfg;
cfg.path = dir;
cfg.name = "house-fixture";
cfg.outputName = "house-fixture";
cfg.target = HostTarget();
std::array<fs::path, 0> ifaces = {};
std::array<fs::path, 1> impls = { "f" };
cfg.GetInterfacesAndImplementations(ifaces, impls);
ProjectLint::AddProjectLintRules(cfg);
RunLintOptions opts;
opts.mode = mode;
opts.globs = { std::string(rule) };
RuleRun run;
run.summary = RunLint(cfg, opts);
std::ifstream f(dir / "f.cpp", std::ios::binary);
std::stringstream buffer;
buffer << f.rdbuf();
run.text = std::move(buffer).str();
return run;
}
bool HasFinding(const LintSummary& s, std::string_view fragment) {
return std::any_of(s.findings.begin(), s.findings.end(), [&](const LintFinding& f) { return f.message.contains(fragment); });
}
}
int main() {
// fixed-width-types: int/long long convert; main/argc and extern "C" stay.
{
RuleRun r = RunRule("int Foo(long long v) { int x = 5; return x; }\n" "extern \"C\" int setenv(const char* n, const char* v, int o);\n" "int main(int argc, char** argv) { return 0; }\n", "fixed-width-types", LintMode::Apply);
Check(r.text.contains("std::int32_t Foo(std::int64_t v) { std::int32_t x = 5;"), "fixed-width converts int and long long");
Check(r.text.contains("extern \"C\" int setenv"), "extern \"C\" prototype keeps int");
Check(r.text.contains("int main(int argc"), "main/argc keep int");
}
// fixed-width-types is signed/unsigned aware, in any specifier order;
// bare char (text) and long double (floating) are not integers.
{
RuleRun r = RunRule("void F() {\n"
" unsigned a = 1;\n"
" unsigned int b = 2;\n"
" unsigned long long c = 3;\n"
" long unsigned int d = 4;\n"
" unsigned short e = 5;\n"
" signed f = 6;\n"
" signed char g = 7;\n"
" auto h = static_cast<unsigned char>(g);\n"
" char text = 'x';\n"
" long double pi = 3.14L;\n"
"}\n",
"fixed-width-types", LintMode::Apply);
Check(r.text.contains("std::uint32_t a = 1;"), "bare unsigned -> uint32");
Check(r.text.contains("std::uint32_t b = 2;"), "unsigned int -> uint32");
Check(r.text.contains("std::uint64_t c = 3;"), "unsigned long long -> uint64");
Check(r.text.contains("std::uint64_t d = 4;"), "long unsigned int (reordered) -> uint64");
Check(r.text.contains("std::uint16_t e = 5;"), "unsigned short -> uint16");
Check(r.text.contains("std::int32_t f = 6;"), "bare signed -> int32");
Check(r.text.contains("std::int8_t g = 7;"), "signed char -> int8");
Check(r.text.contains("static_cast<std::uint8_t>(g)"), "unsigned char -> uint8");
Check(r.text.contains("char text = 'x';"), "bare char stays (text, not an integer)");
Check(r.text.contains("long double pi = 3.14L;"), "long double stays (floating type)");
}
// brace-style: Allman brace joins; standalone scope block stays.
{
RuleRun r = RunRule("void Foo()\n{\n}\n\nvoid Bar() {\n Baz();\n {\n Qux();\n }\n}\n", "brace-style", LintMode::Apply);
Check(r.text.contains("void Foo() {"), "Allman brace joined onto header");
Check(r.text.contains("Baz();\n {"), "scope block brace untouched");
}
// if-single-line: short body joins; braced body stays.
{
RuleRun r = RunRule("void F(bool b) {\n if (b)\n Run();\n if (b) {\n Run();\n }\n}\n", "if-single-line", LintMode::Apply);
Check(r.text.contains("if (b) Run();"), "single-statement if body joined");
Check(r.text.contains("if (b) {"), "braced if body untouched");
}
// single-declaration: simple multi-decl splits; pointer decl only reports.
{
RuleRun r = RunRule("void F() {\n bool a = false, b = true;\n}\n", "single-declaration", LintMode::Apply);
Check(r.text.contains("bool a = false;\n bool b = true;"), "multi-declaration split");
}
// wrap-join: short wrapped call joins; operator chain joins; long stays.
{
RuleRun r = RunRule("void F() {\n G(alpha,\n beta);\n bool x = alpha\n && beta;\n}\n", "wrap-join", LintMode::Apply);
Check(r.text.contains("G(alpha, beta);"), "wrapped call arguments joined");
Check(r.text.contains("bool x = alpha && beta;"), "operator continuation joined");
}
// format-concat: literal-adjacent + rewrites; += RHS rewrites; args survive.
{
RuleRun r = RunRule("void F(std::string name, std::string cmd) {\n"
" std::string a = name + \".cpp\";\n"
" std::string b = \"pre-\" + name + \"-post\";\n"
" cmd += \" \" + name;\n"
" std::string keep = name + cmd;\n"
" fs::path p;\n"
" std::string c = p.stem().string() + \".pcm\";\n"
" fs::path d = std::string(cmd) + \".so\";\n"
"}\n",
"format-concat", LintMode::Apply);
Check(r.text.contains("std::string a = std::format(\"{}.cpp\", name);"), "trailing literal converts");
Check(!r.text.contains("namestd::format"), "replacement splices at the operand boundary");
Check(r.text.contains("std::string b = std::format(\"pre-{}-post\", name);"), "sandwich chain converts");
Check(r.text.contains("cmd += std::format(\" {}\", name);"), "+= RHS converts");
Check(r.text.contains("std::string keep = name + cmd;"), "literal-free + is left alone");
Check(r.text.contains("std::string c = std::format(\"{}.pcm\", p.stem().string());"), "call-chain left operand consumed whole (the stringstd regression)");
Check(r.text.contains("fs::path d = std::format(\"{}.so\", std::string(cmd));"), "constructor-call left operand consumed whole");
}
{
// Ternary and raw-string lines are reported, never rewritten.
RuleRun r = RunRule("std::string F(bool b, std::string n) { return b ? n + \".x\" : n; }\n", "format-concat", LintMode::Report);
Check(HasFinding(r.summary, "not auto-fixable"), "ternary concat reports instead of fixing");
Check(r.text.contains("n + \".x\""), "ternary concat untouched on disk");
}
// paren-spacing: single-space padding removed; wrapped call ends keep.
{
RuleRun r = RunRule("void F() {\n G( x );\n}\n", "paren-spacing", LintMode::Apply);
Check(r.text.contains("G(x);"), "paren padding removed");
}
// naming: wrong-case function/type/static/global flagged; camel local passes.
{
RuleRun r = RunRule("namespace {\n"
" std::int32_t bad_global = 0;\n"
" struct lint_thing {};\n"
"}\n"
"void lower_func() {\n"
" static bool lowerStatic = false;\n"
" std::int32_t fineLocal = 1;\n"
"}\n",
"naming", LintMode::Report);
Check(HasFinding(r.summary, "'bad_global' should be PascalCase"), "lowercase global flagged");
Check(HasFinding(r.summary, "'lint_thing' should be PascalCase"), "lowercase type flagged");
Check(HasFinding(r.summary, "'lower_func' should be PascalCase"), "lowercase function flagged");
Check(HasFinding(r.summary, "'lowerStatic' should be PascalCase"), "camel static flagged");
Check(!HasFinding(r.summary, "fineLocal"), "camelCase local passes");
}
{
// Statement calls with inline lambda arguments are NOT function
// definitions (the any_of regression); a genuine lowercase one-liner
// method still is.
RuleRun r = RunRule("struct Holder {\n"
" bool clean() const { return true; }\n"
"};\n"
"void T(std::vector<std::int32_t>& v) {\n"
" bool x = std::any_of(v.begin(), v.end(), [](std::int32_t n) { return n > 0; });\n"
" std::erase_if(v, [](std::int32_t n) { return n < 0; });\n"
"}\n",
"naming", LintMode::Report);
Check(!HasFinding(r.summary, "any_of"), "call with lambda argument is not a function definition");
Check(!HasFinding(r.summary, "erase_if"), "bare statement call is not a function definition");
Check(HasFinding(r.summary, "'clean' should be PascalCase"), "lowercase one-liner method still flagged");
}
// enum-class + no-iostream-print reports.
{
RuleRun r = RunRule("enum Color { Red };\n", "enum-class", LintMode::Report);
Check(HasFinding(r.summary, "enum class"), "plain enum flagged");
}
{
RuleRun r = RunRule("void F() { std::cout << 1; }\n", "no-iostream-print", LintMode::Report);
Check(HasFinding(r.summary, "std::println"), "std::cout flagged");
}
// The whole ruleset is idempotent: a second Apply changes nothing.
{
std::string_view source = "int Foo()\n{\n std::string s = std::string(\"a\") + \"b\";\n if (true)\n return 1;\n return 0;\n}\n";
static constexpr std::string_view AllRules = "*";
RuleRun first = RunRule(source, AllRules, LintMode::Apply);
RuleRun second = RunRule(first.text, AllRules, LintMode::Apply);
Check(second.summary.changedFiles.empty(), "second apply of the full ruleset is a no-op");
Check(first.text != source, "first apply actually changed the fixture");
}
// Suppression directives against house transforms: the driver reverts
// line-preserving edits (fixed-width) and the count-changing rules check
// Suppressed() themselves (wrap-join).
{
RuleRun r = RunRule("void F() {\n"
" // lint-disable-next-line fixed-width-types\n"
" int keep = 1;\n"
" int convert = 2;\n"
"}\n",
"fixed-width-types", LintMode::Apply);
Check(r.text.contains("int keep = 1;"), "next-line directive keeps the suppressed int");
Check(r.text.contains("std::int32_t convert = 2;"), "unsuppressed line still converts");
}
{
RuleRun r = RunRule("void F() {\n"
" // lint-disable-next-line wrap-join\n"
" G(alpha,\n"
" beta);\n"
" H(alpha,\n"
" beta);\n"
"}\n",
"wrap-join", LintMode::Apply);
Check(r.text.contains("G(alpha,\n"), "suppressed wrap stays wrapped");
Check(r.text.contains("H(alpha, beta);"), "unsuppressed wrap still joins");
}
// wrap-join converges in ONE run: joining the inner paren wrap balances
// the && line, which only then becomes an operator-joinable continuation.
{
RuleRun r = RunRule("void F() {\n"
" bool ok = alpha\n"
" && beta(gamma,\n"
" delta);\n"
"}\n",
"wrap-join", LintMode::Apply);
Check(r.text.contains("bool ok = alpha && beta(gamma, delta);"), "self-enabling joins reach the fixpoint in one apply");
RuleRun second = RunRule(r.text, "wrap-join", LintMode::Apply);
Check(second.summary.changedFiles.empty(), "wrap-join is idempotent after the fixpoint");
}
if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures);
return 1;
}
return 0;
}

View file

@ -0,0 +1,8 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Fixture with no lint violations — rules asserting on dirty.cpp must not
// fire here.
int CleanAnswer() {
return 42;
}

View file

@ -0,0 +1,13 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Fixture with deliberate violations at fixed line numbers —
// tests/Lint/main.cpp asserts on these exact lines. Do not reflow.
// MARKER inside a comment: must be invisible to CommentStripped() (line 7)
int DirtyTab() {
return 1; // deliberate tab indent (line 9)
}
const char* dirtyString = "MARKER inside a string literal (line 12)";
int MARKER_in_code = 13; // identifier survives CommentStripped() (line 13)

435
tests/Lint/main.cpp Normal file
View file

@ -0,0 +1,435 @@
// 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");
}
if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures);
return 1;
}
return 0;
}

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module Greeter;
import std;

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Greeter;

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
@ -28,15 +28,11 @@ int main() {
return 1;
}
if (cfg.implementations[0].moduleDependencies.size() != 1) {
std::println(std::cerr,
"expected main.cpp to depend on 1 module, got {}",
cfg.implementations[0].moduleDependencies.size());
std::println(std::cerr, "expected main.cpp to depend on 1 module, got {}", cfg.implementations[0].moduleDependencies.size());
return 1;
}
if (cfg.implementations[0].moduleDependencies[0]->name != "Greeter") {
std::println(std::cerr,
"expected dep 'Greeter', got '{}'",
cfg.implementations[0].moduleDependencies[0]->name);
std::println(std::cerr, "expected dep 'Greeter', got '{}'", cfg.implementations[0].moduleDependencies[0]->name);
return 1;
}
@ -56,8 +52,7 @@ int main() {
auto run = RunCommandWithTimeout(bin.string(), std::chrono::seconds(10));
if (run.exitCode != 0 || run.timedOut || run.crashed) {
std::println(std::cerr, "binary did not exit cleanly: exit={} output={}",
run.exitCode, run.output);
std::println(std::cerr, "binary did not exit cleanly: exit={} output={}", run.exitCode, run.output);
return 1;
}
if (run.output != "ok-from-module") {

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
@ -7,28 +7,24 @@ namespace fs = std::filesystem;
using namespace Crafter;
namespace {
int failures = 0;
std::int32_t Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++failures;
++Failures;
}
}
// Write a tiny POSIX shell script that exits with `code` and return its
// path. The harness invokes `<binary> <args>` through the host shell, so
// a #!/bin/sh script works as a stand-in for a real test binary.
fs::path WriteExitScript(const fs::path& dir, std::string_view stem, int code) {
fs::path WriteExitScript(const fs::path& dir, std::string_view stem, std::int32_t code) {
fs::path script = dir / stem;
std::ofstream out(script);
out << "#!/bin/sh\nexit " << code << "\n";
out.close();
fs::permissions(script,
fs::perms::owner_read | fs::perms::owner_write | fs::perms::owner_exec
| fs::perms::group_read | fs::perms::group_exec
| fs::perms::others_read | fs::perms::others_exec,
fs::perm_options::replace);
fs::permissions(script, fs::perms::owner_read | fs::perms::owner_write | fs::perms::owner_exec | fs::perms::group_read | fs::perms::group_exec | fs::perms::others_read | fs::perms::others_exec, fs::perm_options::replace);
return script;
}
}
@ -82,8 +78,8 @@ int main() {
fs::remove_all(dir, ec);
if (failures > 0) {
std::println(std::cerr, "{} assertions failed", failures);
if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures);
return 1;
}
return 0;

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;

View file

@ -1,17 +1,17 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
using namespace Crafter;
namespace {
int failures = 0;
std::int32_t Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++failures;
++Failures;
}
}
}
@ -83,8 +83,8 @@ int main() {
Check(!q.Get("--other=").has_value(), "ArgQuery::Get returns nullopt for absent prefix");
}
if (failures > 0) {
std::println(std::cerr, "{} assertions failed", failures);
if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures);
return 1;
}
return 0;

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
export module Greet;
import std;

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
@ -13,12 +13,12 @@ extern "C" int setenv(const char* name, const char* value, int overwrite);
extern "C" int unsetenv(const char* name);
namespace {
int failures = 0;
std::int32_t Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++failures;
++Failures;
}
}
}
@ -84,8 +84,7 @@ int main() {
cfg.target = "aarch64-linux-gnu";
cfg.sysroot = "/opt/alarm-sysroot";
TestRunner r = TestRunner::ForTarget(cfg);
Check(r.exec.find("QEMU_LD_PREFIX=/opt/alarm-sysroot") != std::string::npos,
"ForTarget propagates sysroot to QEMU_LD_PREFIX");
Check(r.exec.find("QEMU_LD_PREFIX=/opt/alarm-sysroot") != std::string::npos, "ForTarget propagates sysroot to QEMU_LD_PREFIX");
}
{
// From a Linux host the only sensible way to run x86_64-w64-mingw32 is wine.
@ -101,17 +100,17 @@ int main() {
{
// Use a unique fake triple so we don't stomp on a real env var the
// CI/dev shell may have set.
const char* name = "CRAFTER_BUILD_RUNNER_fake_target_for_unit_test";
setenv(name, "cmd:fake-tool", 1);
const std::string name = "CRAFTER_BUILD_RUNNER_fake_target_for_unit_test";
setenv(name.c_str(), "cmd:fake-tool", 1);
TestRunner r = TestRunner::FromEnv("fake-target-for-unit-test", TestRunner::Local());
Check(r.name == "cmd:fake-tool", "FromEnv reads CRAFTER_BUILD_RUNNER_<target>");
unsetenv(name);
unsetenv(name.c_str());
TestRunner fallback = TestRunner::FromEnv("fake-target-for-unit-test", TestRunner::Local());
Check(fallback.IsLocal(), "FromEnv falls back when env var is unset");
}
if (failures > 0) {
std::println(std::cerr, "{} assertions failed", failures);
if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures);
return 1;
}
return 0;

View file

@ -1,17 +1,17 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
using namespace Crafter;
namespace {
int failures = 0;
std::int32_t Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++failures;
++Failures;
}
}
@ -115,8 +115,8 @@ int main() {
Check(a.BinDir() != b.BinDir(), "different VariantId yields different BinDir");
}
if (failures > 0) {
std::println(std::cerr, "{} assertions failed", failures);
if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures);
return 1;
}
return 0;

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
@ -41,9 +41,7 @@ int main() {
} else if (fs::exists(systemHome / "wasi-runtime" / "runtime.js")) {
setenv("CRAFTER_BUILD_HOME", systemHome.c_str(), 1);
} else {
std::println(std::cerr,
"wasi-runtime assets not found near {} or {}",
repoWasi.string(), systemHome.string());
std::println(std::cerr, "wasi-runtime assets not found near {} or {}", repoWasi.string(), systemHome.string());
return 77; // skipped — environmental, not a code defect
}

View file

@ -1,5 +1,5 @@
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
@ -15,9 +15,9 @@ extern "C" int setenv(const char* name, const char* value, int overwrite);
// actual multi-.wasm production is exercised by a real wasm build (manual /
// integration), not here — same boundary the WasiBrowserRuntime test draws.
namespace {
int failures = 0;
std::int32_t Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) { std::println(std::cerr, "FAIL: {}", msg); ++failures; }
if (!cond) { std::println(std::cerr, "FAIL: {}", msg); ++Failures; }
}
}
@ -68,7 +68,7 @@ int main() {
setenv("CRAFTER_BUILD_HOME", systemHome.c_str(), 1);
} else {
std::println(std::cerr, "wasi-runtime assets not found; skipping manifest check");
return failures ? 1 : 77;
return Failures ? 1 : 77;
}
Configuration cfg;
@ -103,15 +103,14 @@ int main() {
// prefers it when probes pass.
auto relaxedPos = j.find("relaxed-simd");
auto baselinePos = j.find("\"wvhelper.wasm\"");
Check(relaxedPos != std::string::npos && baselinePos != std::string::npos && relaxedPos < baselinePos,
"relaxed-simd entry precedes baseline");
Check(relaxedPos != std::string::npos && baselinePos != std::string::npos && relaxedPos < baselinePos, "relaxed-simd entry precedes baseline");
}
fs::remove_all(cfg.path, ec);
}
if (failures > 0) {
std::println(std::cerr, "{} assertions failed", failures);
if (Failures > 0) {
std::println(std::cerr, "{} assertions failed", Failures);
return 1;
}
return 0;