- persist TU-owned graphs and guarded configuration evidence - add closed-world production reachability classification - calibrate detector precision and reviewed self-scan policy - terminate poisoned workers instead of longjmp recovery
198 lines
8.9 KiB
C++
198 lines
8.9 KiB
C++
#include <catch2/catch_test_macros.hpp>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <regex>
|
|
#include <string>
|
|
|
|
#include "fold/config/configuration_profile.hpp"
|
|
|
|
namespace {
|
|
|
|
using fold::config::AssumptionKind;
|
|
using fold::config::ConfigurationProfileErrorCode;
|
|
using fold::config::GetenvState;
|
|
using fold::config::parseConfigurationProfileText;
|
|
|
|
constexpr const char* kValidProfile = R"({
|
|
"version": 1,
|
|
"entrypoints": ["worker", "main"],
|
|
"assumptions": [
|
|
{"kind":"accessor", "selector":"Config::enabled", "value":false},
|
|
{"kind":"getenv", "selector":"FEATURE", "state":"unset"},
|
|
{"kind":"entry_arg", "entrypoint":"main", "index":0, "value":-7},
|
|
{"kind":"global", "selector":"g_ready", "value":true},
|
|
{"kind":"getenv", "selector":"LIMIT", "state":"set"}
|
|
]
|
|
})";
|
|
|
|
bool hasError(const fold::config::ConfigurationProfileParseResult& result,
|
|
ConfigurationProfileErrorCode code) {
|
|
for (const auto& error : result.errors) {
|
|
if (error.code == code)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
struct TempFile {
|
|
std::filesystem::path path =
|
|
std::filesystem::temp_directory_path() /
|
|
("fold-configuration-profile-" +
|
|
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".json");
|
|
|
|
explicit TempFile(const std::string& contents) { std::ofstream(path) << contents; }
|
|
~TempFile() { std::filesystem::remove(path); }
|
|
|
|
TempFile(const TempFile&) = delete;
|
|
TempFile& operator=(const TempFile&) = delete;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("configuration profile parses all v1 assumption kinds and typed values",
|
|
"[configuration-profile]") {
|
|
const auto result = parseConfigurationProfileText(kValidProfile);
|
|
REQUIRE(result);
|
|
REQUIRE(result.profile->version() == 1);
|
|
REQUIRE(result.profile->entrypoints() == std::vector<std::string>{"main", "worker"});
|
|
REQUIRE(result.profile->assumptions().size() == 5);
|
|
|
|
bool saw_bool = false;
|
|
bool saw_int = false;
|
|
bool saw_unset = false;
|
|
for (const auto& assumption : result.profile->assumptions()) {
|
|
REQUIRE(std::regex_match(assumption.symbolId().value(), std::regex("[0-9a-f]{64}")));
|
|
if (assumption.value && std::holds_alternative<bool>(*assumption.value))
|
|
saw_bool = true;
|
|
if (assumption.value && std::holds_alternative<std::int64_t>(*assumption.value))
|
|
saw_int = true;
|
|
if (assumption.kind == AssumptionKind::Getenv) {
|
|
REQUIRE_FALSE(assumption.value.has_value());
|
|
if (assumption.getenv_state == GetenvState::Unset)
|
|
saw_unset = true;
|
|
}
|
|
}
|
|
REQUIRE(saw_bool);
|
|
REQUIRE(saw_int);
|
|
REQUIRE(saw_unset);
|
|
}
|
|
|
|
TEST_CASE("configuration profile rejects root and assumption schema drift",
|
|
"[configuration-profile][strict]") {
|
|
const auto missing = parseConfigurationProfileText(R"({"entrypoints":[],"assumptions":[]})");
|
|
REQUIRE_FALSE(missing);
|
|
REQUIRE(hasError(missing, ConfigurationProfileErrorCode::MissingField));
|
|
|
|
const auto future =
|
|
parseConfigurationProfileText(R"({"version":2,"entrypoints":[],"assumptions":[]})");
|
|
REQUIRE_FALSE(future);
|
|
REQUIRE(hasError(future, ConfigurationProfileErrorCode::UnsupportedVersion));
|
|
|
|
const auto unknown = parseConfigurationProfileText(
|
|
R"({"version":1,"entrypoints":[],"assumptions":[],"extra":true})");
|
|
REQUIRE_FALSE(unknown);
|
|
REQUIRE(hasError(unknown, ConfigurationProfileErrorCode::UnknownField));
|
|
|
|
const auto nested_unknown = parseConfigurationProfileText(
|
|
R"({"version":1,"entrypoints":[],"assumptions":[{"kind":"global","selector":"g","value":true,"extra":0}]})");
|
|
REQUIRE_FALSE(nested_unknown);
|
|
REQUIRE(hasError(nested_unknown, ConfigurationProfileErrorCode::UnknownField));
|
|
}
|
|
|
|
TEST_CASE("configuration profile performs no value or index coercion",
|
|
"[configuration-profile][strict]") {
|
|
const auto string_bool = parseConfigurationProfileText(
|
|
R"({"version":1,"entrypoints":[],"assumptions":[{"kind":"global","selector":"g","value":"true"}]})");
|
|
REQUIRE_FALSE(string_bool);
|
|
REQUIRE(hasError(string_bool, ConfigurationProfileErrorCode::InvalidType));
|
|
|
|
const auto float_value = parseConfigurationProfileText(
|
|
R"({"version":1,"entrypoints":[],"assumptions":[{"kind":"accessor","selector":"f","value":1.0}]})");
|
|
REQUIRE_FALSE(float_value);
|
|
REQUIRE(hasError(float_value, ConfigurationProfileErrorCode::InvalidType));
|
|
|
|
const auto string_index = parseConfigurationProfileText(
|
|
R"({"version":1,"entrypoints":["main"],"assumptions":[{"kind":"entry_arg","entrypoint":"main","index":"0","value":1}]})");
|
|
REQUIRE_FALSE(string_index);
|
|
REQUIRE(hasError(string_index, ConfigurationProfileErrorCode::InvalidType));
|
|
}
|
|
|
|
TEST_CASE("configuration profile validates selectors, entry arguments, getenv, and duplicates",
|
|
"[configuration-profile][strict]") {
|
|
const auto empty_selector = parseConfigurationProfileText(
|
|
R"({"version":1,"entrypoints":[],"assumptions":[{"kind":"global","selector":" ","value":true}]})");
|
|
REQUIRE_FALSE(empty_selector);
|
|
REQUIRE(hasError(empty_selector, ConfigurationProfileErrorCode::InvalidValue));
|
|
|
|
const auto unknown_entrypoint = parseConfigurationProfileText(
|
|
R"({"version":1,"entrypoints":["main"],"assumptions":[{"kind":"entry_arg","entrypoint":"worker","index":0,"value":1}]})");
|
|
REQUIRE_FALSE(unknown_entrypoint);
|
|
REQUIRE(hasError(unknown_entrypoint, ConfigurationProfileErrorCode::UnknownEntrypoint));
|
|
|
|
const auto negative_index = parseConfigurationProfileText(
|
|
R"({"version":1,"entrypoints":["main"],"assumptions":[{"kind":"entry_arg","entrypoint":"main","index":-1,"value":1}]})");
|
|
REQUIRE_FALSE(negative_index);
|
|
REQUIRE(hasError(negative_index, ConfigurationProfileErrorCode::InvalidValue));
|
|
|
|
const auto invalid_getenv = parseConfigurationProfileText(
|
|
R"({"version":1,"entrypoints":[],"assumptions":[{"kind":"getenv","selector":"X","state":"unset","value":1}]})");
|
|
REQUIRE_FALSE(invalid_getenv);
|
|
REQUIRE(hasError(invalid_getenv, ConfigurationProfileErrorCode::UnknownField));
|
|
|
|
const auto duplicate_key = parseConfigurationProfileText(
|
|
R"({"version":1,"version":1,"entrypoints":[],"assumptions":[]})");
|
|
REQUIRE_FALSE(duplicate_key);
|
|
REQUIRE(hasError(duplicate_key, ConfigurationProfileErrorCode::DuplicateField));
|
|
|
|
const auto duplicate_selector = parseConfigurationProfileText(
|
|
R"({"version":1,"entrypoints":[],"assumptions":[{"kind":"global","selector":"g","value":true},{"kind":"global","selector":"g","value":false}]})");
|
|
REQUIRE_FALSE(duplicate_selector);
|
|
REQUIRE(hasError(duplicate_selector, ConfigurationProfileErrorCode::DuplicateAssumption));
|
|
}
|
|
|
|
TEST_CASE("configuration profile canonical JSON and hash ignore input order and whitespace",
|
|
"[configuration-profile][canonical]") {
|
|
const auto first = parseConfigurationProfileText(R"({
|
|
"version":1,
|
|
"entrypoints":["worker","main"],
|
|
"assumptions":[
|
|
{"kind":"global","selector":"z","value":9},
|
|
{"kind":"entry_arg","entrypoint":"main","index":0,"value":true}
|
|
]
|
|
})");
|
|
const auto second = parseConfigurationProfileText(
|
|
R"({"assumptions":[{"value":true,"index":0,"entrypoint":"main","kind":"entry_arg"},{"selector":"z","value":9,"kind":"global"}],"entrypoints":["main","worker"],"version":1})");
|
|
REQUIRE(first);
|
|
REQUIRE(second);
|
|
REQUIRE(first.profile->canonicalJsonText() == second.profile->canonicalJsonText());
|
|
REQUIRE(first.profile->canonicalHash() == second.profile->canonicalHash());
|
|
REQUIRE(std::regex_match(first.profile->canonicalHash(), std::regex("[0-9a-f]{64}")));
|
|
|
|
REQUIRE(first.profile->assumptions().size() == second.profile->assumptions().size());
|
|
for (std::size_t index = 0; index < first.profile->assumptions().size(); ++index) {
|
|
REQUIRE(first.profile->assumptions()[index].symbolId() ==
|
|
second.profile->assumptions()[index].symbolId());
|
|
}
|
|
}
|
|
|
|
TEST_CASE("configuration profile parses files and reports bounded structured errors",
|
|
"[configuration-profile][file]") {
|
|
TempFile file(kValidProfile);
|
|
const auto parsed = fold::config::parseConfigurationProfileFile(file.path);
|
|
REQUIRE(parsed);
|
|
|
|
std::string invalid = R"({"version":1,"entrypoints":[],"assumptions":[])";
|
|
const auto syntax_error = parseConfigurationProfileText(invalid);
|
|
REQUIRE_FALSE(syntax_error);
|
|
REQUIRE(hasError(syntax_error, ConfigurationProfileErrorCode::JsonSyntax));
|
|
REQUIRE(syntax_error.errors.size() <= fold::config::kMaximumConfigurationProfileErrors);
|
|
REQUIRE_FALSE(syntax_error.errors.front().path.empty());
|
|
REQUIRE_FALSE(syntax_error.errors.front().message.empty());
|
|
|
|
const auto missing_file =
|
|
fold::config::parseConfigurationProfileFile(file.path.string() + ".missing");
|
|
REQUIRE_FALSE(missing_file);
|
|
REQUIRE(hasError(missing_file, ConfigurationProfileErrorCode::FileRead));
|
|
}
|