Crafter.Review/tests/Text/main.cpp
Jorijn van der Graaf 36852edf63
feat: the crafter-review skeleton, and its semantics for text, JSON and crypto
What the rest of crafter-review stands on.

- project.cpp: a static crafter.review-lib and the crafter-review
  executable; Crafter.Network without QUIC, pinned, or with --local from
  the checkout next to this one; Crafter.LintRules' lite rules, pinned;
  the version from CRAFTER_REVIEW_VERSION, git describe or 0.0.0-dev.
- :Core and :Driver: the version, PromptVersion 1, --version.
- :Json: nlohmann's ordered_json, which keeps key order and unknown keys,
  so the files every driver on a machine shares survive a rewrite; dumps
  in the indented file layout, the spaced one-line form and the compact
  sorted form the structured-output schema needs byte for byte.
- :Crypto: SHA-256/SHA-1/MD5, HMAC, PBKDF2 through libcrypto; base64 and
  base32 written out, because what they refuse, and with which message,
  is pinned.
- :Unicode, Unicode 16.0.0's tables: whitespace, line breaks, decimal
  digits, printable characters, case mappings and case-insensitive folds.
- :Text: string functions on code points, number parsing, the shortest
  float form and half-even rounding, printed values, shell patterns, URL
  splitting and unquoting, POSIX paths, universal newlines and the
  replacing UTF-8 decoder.

Tests: Crypto (published vectors), Text (293 reference vectors).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:29:18 +01:00

220 lines
12 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// The semantics of :Text, checked against reference vectors: every vector in
// fixture/vectors.json is a recorded answer (the file names the Unicode
// version it holds).
//
// The driver's output is decided by these rules: a unit's excerpt is cut by
// Strip() with Unicode whitespace, a key by Length() in code points, a peak
// by Round() half to even, an exclusion by FnMatch()'s character classes, a
// stack key by UrlSplit() and Lower(). The nearest C++ equivalent would agree
// on ASCII and drift exactly where nobody looks.
//
// Also :Json's three dump layouts — DumpFile(), DumpSpaced() and
// DumpCompactSorted() — and its handling of a duplicate key.
import std;
import Crafter.Review;
namespace fs = std::filesystem;
using namespace Crafter::Review;
namespace {
std::int32_t Failures = 0;
void Check(bool cond, std::string_view msg) {
if (!cond) {
std::println(std::cerr, "FAIL: {}", msg);
++Failures;
}
}
std::vector<std::string> Strings(const Json& array) {
std::vector<std::string> out;
for (const Json& item : array) out.push_back(item.get<std::string>());
return out;
}
std::string Show(const std::vector<std::string>& pieces) {
Json array = Json::array();
for (const std::string& piece : pieces) array.push_back(piece);
return Dyn::ReprValue(array);
}
std::string ShowOptional(const std::optional<std::string>& value) {
return value ? Dyn::Repr(*value) : std::string("None");
}
bool SameOptional(const std::optional<std::string>& value, const Json& expected) {
if (expected.is_null()) return !value.has_value();
return value && *value == expected.get<std::string>();
}
bool IsError(const Json& value) {
return value.is_object() && value.contains("error");
}
}
int main() {
const fs::path path = fs::current_path() / "tests" / "Text" / "fixture" / "vectors.json";
const Json vectors = Json::parse(Dyn::ReadText(path));
for (const Json& v : vectors["strip"]) {
const std::string input = v[0];
Check(Dyn::Strip(input) == v[1].get<std::string>(), std::format("strip({}) = {}", Dyn::Repr(input), Dyn::Repr(Dyn::Strip(input))));
}
for (const Json& v : vectors["split"]) {
const std::string input = v[0];
Check(Dyn::Split(input) == Strings(v[1]), std::format("split({}) = {}", Dyn::Repr(input), Show(Dyn::Split(input))));
}
for (const Json& v : vectors["splitlines"]) {
const std::string input = v[0];
Check(Dyn::SplitLines(input) == Strings(v[1]), std::format("splitlines({}) = {}", Dyn::Repr(input), Show(Dyn::SplitLines(input))));
}
for (const Json& v : vectors["splitlinesKeep"]) {
const std::string input = v[0];
Check(Dyn::SplitLines(input, true) == Strings(v[1]), std::format("splitlines({}, keepends) = {}", Dyn::Repr(input), Show(Dyn::SplitLines(input, true))));
}
for (const Json& v : vectors["lower"]) {
const std::string input = v[0];
Check(Dyn::Lower(input) == v[1].get<std::string>(), std::format("lower({}) = {}", Dyn::Repr(input), Dyn::Repr(Dyn::Lower(input))));
}
for (const Json& v : vectors["upper"]) {
const std::string input = v[0];
Check(Dyn::Upper(input) == v[1].get<std::string>(), std::format("upper({}) = {}", Dyn::Repr(input), Dyn::Repr(Dyn::Upper(input))));
}
for (const Json& v : vectors["floatRepr"]) {
const double value = v[0].get<double>();
Check(Dyn::FloatRepr(value) == v[1].get<std::string>(), std::format("repr({}) = {}", v[1].get<std::string>(), Dyn::FloatRepr(value)));
}
for (const Json& v : vectors["round"]) {
const double value = v[0].get<double>();
const std::int32_t digits = v[1].get<std::int32_t>();
const std::string got = digits == 0 ? std::to_string(Dyn::RoundToInt(value)) : Dyn::FloatRepr(Dyn::Round(value, digits));
Check(got == v[2].get<std::string>(), std::format("Round({}, {}) = {}, expected {}", Dyn::FloatRepr(value), digits, got, v[2].get<std::string>()));
}
for (const Json& v : vectors["int"]) {
const std::string input = v[0];
const std::optional<std::int64_t> got = Dyn::ParseInt(input);
if (IsError(v[1])) {
Check(!got && Dyn::IntError(input) == v[1]["message"].get<std::string>(), std::format("ParseInt({}) fails with the recorded message ({})", Dyn::Repr(input), Dyn::IntError(input)));
} else {
Check(got && *got == v[1].get<std::int64_t>(), std::format("int({}) = {}", Dyn::Repr(input), got ? std::to_string(*got) : std::string("error")));
}
}
for (const Json& v : vectors["float"]) {
const std::string input = v[0];
const std::optional<double> got = Dyn::ParseFloat(input);
if (IsError(v[1])) {
Check(!got && Dyn::FloatError(input) == v[1]["message"].get<std::string>(), std::format("ParseFloat({}) fails with the recorded message", Dyn::Repr(input)));
} else {
Check(got && Dyn::FloatRepr(*got) == v[1].get<std::string>(), std::format("float({}) = {}", Dyn::Repr(input), got ? Dyn::FloatRepr(*got) : std::string("error")));
}
}
for (const Json& v : vectors["repr"]) {
const std::string input = v[0];
Check(Dyn::Repr(input) == v[1].get<std::string>(), std::format("repr of {} = {}", v[1].get<std::string>(), Dyn::Repr(input)));
}
for (const char* key : { "reprList", "reprDict" }) {
for (const Json& v : vectors[key]) {
Check(Dyn::ReprValue(v[0]) == v[1].get<std::string>(), std::format("ReprValue = {}, expected {}", Dyn::ReprValue(v[0]), v[1].get<std::string>()));
}
}
for (const Json& v : vectors["fnmatch"]) {
const std::string name = v[0];
const std::string pattern = v[1];
Check(Dyn::FnMatch(name, pattern) == v[2].get<bool>(), std::format("fnmatch({}, {}) should be {}", Dyn::Repr(name), Dyn::Repr(pattern), v[2].get<bool>()));
}
for (const Json& v : vectors["urlsplit"]) {
const std::string url = v[0];
const Json& expected = v[1];
if (IsError(expected)) {
std::string message = "(no error)";
try {
Dyn::UrlSplit(url);
} catch (const std::invalid_argument& e) {
message = e.what();
}
Check(message == expected["message"].get<std::string>(), std::format("UrlSplit({}) fails with {}, expected {}", Dyn::Repr(url), Dyn::Repr(message), Dyn::Repr(expected["message"].get<std::string>())));
continue;
}
const Dyn::SplitUrl got = Dyn::UrlSplit(url);
Check(got.scheme == expected["scheme"].get<std::string>() && got.netloc == expected["netloc"].get<std::string>()
&& got.path == expected["path"].get<std::string>() && got.query == expected["query"].get<std::string>()
&& got.fragment == expected["fragment"].get<std::string>(),
std::format("urlsplit({}) parts: {} {} {} {} {}", Dyn::Repr(url), got.scheme, got.netloc, got.path, got.query, got.fragment));
Check(SameOptional(got.Hostname(), expected["hostname"]), std::format("urlsplit({}).hostname = {}", Dyn::Repr(url), ShowOptional(got.Hostname())));
Check(SameOptional(got.Username(), expected["username"]), std::format("urlsplit({}).username = {}", Dyn::Repr(url), ShowOptional(got.Username())));
Check(SameOptional(got.Password(), expected["password"]), std::format("urlsplit({}).password = {}", Dyn::Repr(url), ShowOptional(got.Password())));
const Json& port = expected["port"];
if (IsError(port)) {
std::string message = "(no error)";
try {
got.Port();
} catch (const std::invalid_argument& e) {
message = e.what();
}
Check(message == port["message"].get<std::string>(), std::format("urlsplit({}).port fails with {}", Dyn::Repr(url), Dyn::Repr(message)));
} else if (port.is_null()) {
Check(!got.Port(), std::format("urlsplit({}).port is None", Dyn::Repr(url)));
} else {
Check(got.Port() == port.get<std::int32_t>(), std::format("urlsplit({}).port", Dyn::Repr(url)));
}
}
for (const Json& v : vectors["unquote"]) {
const std::string input = v[0];
Check(Dyn::Unquote(input) == v[1].get<std::string>(), std::format("unquote({}) = {}", Dyn::Repr(input), Dyn::Repr(Dyn::Unquote(input))));
}
for (const Json& v : vectors["natural"]) {
const std::string name = v[0];
Dyn::NaturalKey expected;
for (const Json& part : v[1]) {
if (part.is_string()) {
expected.parts.emplace_back(part.get<std::string>());
} else {
expected.parts.emplace_back(part.get<std::int64_t>());
}
}
Check(Dyn::Natural(name) == expected, std::format("natural key of {}", Dyn::Repr(name)));
}
for (const Json& v : vectors["paths"]) {
const std::string op = v[0][0];
const std::string a = v[0][1];
std::string got;
if (op == "normpath") got = Dyn::NormPath(a);
if (op == "abspath") got = Dyn::AbsPath(a);
if (op == "basename") got = Dyn::BaseName(a);
if (op == "dirname") got = Dyn::DirName(a);
if (op == "relpath") got = Dyn::RelPath(a, v[0][2].get<std::string>());
if (op == "join") got = Dyn::JoinPath(a, v[0][2].get<std::string>());
Check(got == v[1].get<std::string>(), std::format("{}({}) = {}, expected {}", op, Dyn::Repr(a), Dyn::Repr(got), Dyn::Repr(v[1].get<std::string>())));
}
for (const Json& v : vectors["jsonIndent1"]) {
Check(DumpFile(v[0]) == v[1].get<std::string>(), std::format("DumpFile:\n{}\nexpected:\n{}", DumpFile(v[0]), v[1].get<std::string>()));
}
for (const Json& v : vectors["jsonDefault"]) {
Check(DumpSpaced(v[0]) == v[1].get<std::string>(), std::format("DumpSpaced: {} / expected {}", DumpSpaced(v[0]), v[1].get<std::string>()));
}
for (const Json& v : vectors["jsonSorted"]) {
Check(DumpCompactSorted(v[0]) == v[1].get<std::string>(), std::format("DumpCompactSorted: {} / expected {}", DumpCompactSorted(v[0]), v[1].get<std::string>()));
}
for (const Json& v : vectors["jsonDuplicate"]) {
const Json parsed = Json::parse(v[0].get<std::string>());
Json pairs = Json::array();
for (auto it = parsed.begin(); it != parsed.end(); ++it) pairs.push_back(Json::array({ it.key(), it.value() }));
Check(pairs == v[1], std::format("a duplicate key keeps its first place and its last value: {}", pairs.dump()));
}
// Read the way open() reads: \r\n and a lone \r are \n.
Check(Dyn::UniversalNewlines("a\r\nb\rc\n") == "a\nb\nc\n", "universal newlines");
Check(Dyn::Length("é😀a") == 3 && Dyn::Slice("é😀abc", 1, 3) == "😀a" && Dyn::Slice("abc", -2) == "bc", "len and slicing count code points");
Check(Dyn::DecodeReplace("a\xC3(b") == "a\xEF\xBF\xBD(b", "a bad byte becomes U+FFFD, the next character survives");
Check(!Dyn::IsUtf8("\xED\xA0\x80") && Dyn::IsUtf8("\xEF\xBF\xBD"), "a surrogate is not UTF-8; a real U+FFFD is");
Check(Dyn::IgnoreCaseLetter(U'K') == 'k' && Dyn::IgnoreCaseLetter(U'ſ') == 's' && Dyn::IgnoreCaseLetter(U'é') == '\0', "re.IGNORECASE folds");
Check(Dyn::ErrnoText(111) == "[Errno 111] Connection refused", "OSError text");
Check(Dyn::Count("abab", "ab") == 2 && Dyn::Count("aaa", "aa") == 1 && Dyn::Count("ab", "") == 3, "count: non-overlapping, empty counts len+1");
Check(Dyn::RSplit("a)b)c", ")", 1) == std::vector<std::string>{ "a)b", "c" }, "rsplit with maxsplit");
std::println("{} vector groups, Unicode {}", vectors.size() - 1, vectors["unicode"].get<std::string>());
return Failures == 0 ? 0 : 1;
}