220 lines
12 KiB
C++
220 lines
12 KiB
C++
|
|
// 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;
|
|||
|
|
}
|