This commit is contained in:
parent
a25b0a1ded
commit
8892154b28
70 changed files with 2780 additions and 596 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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
260
tests/HouseRules/main.cpp
Normal 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;
|
||||
}
|
||||
8
tests/Lint/fixture/clean.cpp
Normal file
8
tests/Lint/fixture/clean.cpp
Normal 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;
|
||||
}
|
||||
13
tests/Lint/fixture/dirty.cpp
Normal file
13
tests/Lint/fixture/dirty.cpp
Normal 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
435
tests/Lint/main.cpp
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue