fold/tests/unit/test_patch_writer.cpp
trvon a791230552
feat(report): C9 FixIts + unified-diff --fix-output
Adds the FixIt collector + per-file unified-diff patch writer
behind a new `fold scan --fix-output <path>` flag. Patch-only --
no in-place rewrites -- so a bad suggestion can't silently
overwrite source.

Consumers (only when the transform is provably sound):
- UninitLocalRead: scalar integer locals only.
  `int x;` -> `int x = 0;`. Non-scalar types deferred.
- DeprecatedApi: regex extracts `[Uu]se foo()` from the
  deprecation message; silent skip when unparseable rather
  than hallucinating a rewrite.

Patches include 3 lines of context above + below each fix so
`git apply --check` accepts them. End-to-end verified against a
synthetic fixture.
2026-05-25 16:40:43 -04:00

112 lines
4.1 KiB
C++

// Matrix row C9 — confirm the patch writer emits a unified diff
// that `git apply` can consume.
//
// Test pins:
// 1. A FixIt that replaces a single line emits a hunk with one
// `-` and one `+` line, valid `@@` header.
// 2. FixIts on disjoint files produce one hunk per file in the
// same patch.
// 3. FixIts ordered out-of-line get sorted by start_line.
// 4. Missing source file produces no hunk (no abort).
#include <catch2/catch_test_macros.hpp>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <string>
#include "fold/report/finding.hpp"
#include "fold/report/patch_writer.hpp"
namespace {
std::filesystem::path writeTempFile(const std::string& name, const std::string& contents) {
auto p = std::filesystem::temp_directory_path() / name;
std::ofstream out(p);
out << contents;
out.close();
return p;
}
fold::Finding makeFinding(std::string file, int line, int col, std::string replacement,
std::string desc) {
fold::Finding f;
f.kind = fold::FindingKind::UninitLocalRead;
fold::FixIt fix;
fix.file = std::move(file);
fix.start_line = line;
fix.start_column = col;
fix.end_line = line;
fix.end_column = col;
fix.replacement = std::move(replacement);
fix.description = std::move(desc);
f.fix_its.push_back(std::move(fix));
return f;
}
} // namespace
TEST_CASE("patch_writer: single-line in-place insertion emits a valid hunk",
"[c9][patch_writer][single]") {
auto file = writeTempFile("c9_single.c", "int x;\n");
fold::Finding f =
makeFinding(file.filename().string(), 1, 6, " = 0", "Initialise uninitialised local");
f.fix_its[0].end_column = 6; // pure insertion before `;`
auto patch =
fold::report::writeFixItPatch({f}, std::filesystem::temp_directory_path().string());
REQUIRE(patch.find("--- a/c9_single.c") != std::string::npos);
REQUIRE(patch.find("+++ b/c9_single.c") != std::string::npos);
REQUIRE(patch.find("@@") != std::string::npos);
REQUIRE(patch.find("-int x;") != std::string::npos);
REQUIRE(patch.find("+int x = 0;") != std::string::npos);
std::filesystem::remove(file);
}
TEST_CASE("patch_writer: FixIts on two files produce two hunks", "[c9][patch_writer][multi-file]") {
auto a = writeTempFile("c9_a.c", "int a;\n");
auto b = writeTempFile("c9_b.c", "int b;\n");
fold::Finding fa = makeFinding(a.filename().string(), 1, 6, " = 0", "init a");
fa.fix_its[0].end_column = 6;
fold::Finding fb = makeFinding(b.filename().string(), 1, 6, " = 0", "init b");
fb.fix_its[0].end_column = 6;
auto patch =
fold::report::writeFixItPatch({fa, fb}, std::filesystem::temp_directory_path().string());
REQUIRE(patch.find("--- a/c9_a.c") != std::string::npos);
REQUIRE(patch.find("--- a/c9_b.c") != std::string::npos);
std::filesystem::remove(a);
std::filesystem::remove(b);
}
TEST_CASE("patch_writer: FixIts within one file are sorted by line", "[c9][patch_writer][sort]") {
auto file = writeTempFile("c9_sort.c", "int a;\nint b;\nint c;\n");
// Emit in reverse order — writer must sort.
fold::Finding f3 = makeFinding(file.filename().string(), 3, 6, " = 0", "init c");
f3.fix_its[0].end_column = 6;
fold::Finding f1 = makeFinding(file.filename().string(), 1, 6, " = 0", "init a");
f1.fix_its[0].end_column = 6;
auto patch =
fold::report::writeFixItPatch({f3, f1}, std::filesystem::temp_directory_path().string());
auto line1_pos = patch.find("-int a;");
auto line3_pos = patch.find("-int c;");
REQUIRE(line1_pos != std::string::npos);
REQUIRE(line3_pos != std::string::npos);
REQUIRE(line1_pos < line3_pos);
std::filesystem::remove(file);
}
TEST_CASE("patch_writer: missing source file is skipped, not fatal",
"[c9][patch_writer][missing]") {
fold::Finding f = makeFinding("nonexistent_c9_file.c", 1, 1, "X", "noop");
auto patch =
fold::report::writeFixItPatch({f}, std::filesystem::temp_directory_path().string());
// Either an empty string or one with no hunk for the missing path.
REQUIRE(patch.find("--- a/nonexistent_c9_file.c") == std::string::npos);
}