Skip to content

Commit 23c5843

Browse files
committed
test: config.toml's overrides are read by a function a test can call
The third place an override may be stated was parsed inside `load_or_init`, which bootstraps a home, so no unit test could reach it. `parse_payload_overrides` is now a pure function of the parsed document, and three cases cover the two shapes it accepts, the key it refuses, and a config with no such table.
1 parent 1143154 commit 23c5843

2 files changed

Lines changed: 92 additions & 30 deletions

File tree

‎src/config.cppm‎

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,13 @@ std::expected<GlobalConfig, ConfigError> load_or_init(
315315
// Pretty-print resolved config for `mcpp env` command.
316316
void print_env(const GlobalConfig& cfg);
317317

318+
// `[xlings.overrides]` of a parsed config.toml, keyed by package identity
319+
// (`<ns>:<name>`, the namespace defaulted to `xim`): where a declared xlings
320+
// payload comes from on this machine (mcpp#755). A pure function of the
321+
// document, so a test reads it without a home to bootstrap.
322+
std::expected<std::map<std::string, GlobalConfig::PayloadOverride>, std::string>
323+
parse_payload_overrides(const mcpp::libs::toml::Document& doc);
324+
318325
// Normalize legacy index naming in a loaded config (exported for tests):
319326
// org migration mcpp-community/mcpp-index -> mcpplibs/mcpp-index, index name
320327
// mcpp-index -> mcpplibs, then name+url dedup. Order matters: URL first, so
@@ -652,6 +659,43 @@ void canonicalize_legacy_index_names(GlobalConfig& cfg) {
652659
cfg.indexRepos = std::move(normalized);
653660
}
654661

662+
std::expected<std::map<std::string, GlobalConfig::PayloadOverride>, std::string>
663+
parse_payload_overrides(const mcpp::libs::toml::Document& doc) {
664+
std::map<std::string, GlobalConfig::PayloadOverride> out;
665+
auto* ot = doc.get_table("xlings.overrides");
666+
if (!ot) return out;
667+
for (auto const& [key, val] : *ot) {
668+
GlobalConfig::PayloadOverride o;
669+
o.line = static_cast<int>(val.position.line);
670+
if (val.is_string()) {
671+
o.value = val.as_string();
672+
} else if (val.is_table()) {
673+
for (auto const& [k, v] : val.as_table()) {
674+
if ((k != "program" && k != "root" && k != "version") || !v.is_string())
675+
return std::unexpected(std::format(
676+
"config.toml [xlings.overrides] {}: '{}' is not a key of an "
677+
"override; an override is a path, or a table of `program` "
678+
"or `root` and an optional `version`", key, k));
679+
if (k == "version") o.version = v.as_string();
680+
else { o.kind = k; o.value = v.as_string(); }
681+
}
682+
}
683+
if (o.value.empty())
684+
return std::unexpected(std::format(
685+
"config.toml [xlings.overrides] {}: expected a path, or a table "
686+
"naming `program` or `root`", key));
687+
// The identity, as the manifest and the address set spell it: a key
688+
// without a namespace names the `xim` package, and a version in the key
689+
// is dropped -- an override states where a package comes from, not
690+
// which version of it is wanted.
691+
auto colon = key.find(':');
692+
std::string ident = colon == std::string::npos ? "xim:" + key : key;
693+
if (auto at = ident.find('@'); at != std::string::npos) ident.resize(at);
694+
out.insert_or_assign(std::move(ident), std::move(o));
695+
}
696+
return out;
697+
}
698+
655699
std::expected<GlobalConfig, ConfigError> load_or_init(
656700
bool quiet,
657701
BootstrapProgressCallback onBootstrapProgress,
@@ -731,36 +775,11 @@ std::expected<GlobalConfig, ConfigError> load_or_init(
731775
cfg.defaultJobs = doc->get_int("build.default_jobs").value_or(0);
732776
cfg.defaultToolchain = doc->get_string("toolchain.default").value_or("");
733777
cfg.defaultTarget = doc->get_string("toolchain.default_target").value_or("");
734-
// [xlings.overrides] (mcpp#755). Read in the manifest's two shapes -- a
735-
// path, or a table of `program` or `root` and `version` -- and refused by
736-
// name when it is neither, as a malformed config.toml key always is.
737-
if (auto* ot = doc->get_table("xlings.overrides")) {
738-
for (auto const& [key, val] : *ot) {
739-
GlobalConfig::PayloadOverride o;
740-
o.line = static_cast<int>(val.position.line);
741-
if (val.is_string()) {
742-
o.value = val.as_string();
743-
} else if (val.is_table()) {
744-
for (auto const& [k, v] : val.as_table()) {
745-
if ((k != "program" && k != "root" && k != "version") || !v.is_string())
746-
return std::unexpected(ConfigError{std::format(
747-
"config.toml [xlings.overrides] {}: '{}' is not a key of an "
748-
"override; an override is a path, or a table of `program` "
749-
"or `root` and an optional `version`", key, k)});
750-
if (k == "version") o.version = v.as_string();
751-
else { o.kind = k; o.value = v.as_string(); }
752-
}
753-
}
754-
if (o.value.empty())
755-
return std::unexpected(ConfigError{std::format(
756-
"config.toml [xlings.overrides] {}: expected a path, or a table "
757-
"naming `program` or `root`", key)});
758-
auto colon = key.find(':');
759-
std::string ident = colon == std::string::npos ? "xim:" + key : key;
760-
if (auto at = ident.find('@'); at != std::string::npos) ident.resize(at);
761-
cfg.payloadOverrides.insert_or_assign(std::move(ident), std::move(o));
762-
}
763-
}
778+
// [xlings.overrides] (mcpp#755), read by the same function the tests read.
779+
if (auto overrides = parse_payload_overrides(*doc); overrides)
780+
cfg.payloadOverrides = std::move(*overrides);
781+
else
782+
return std::unexpected(ConfigError{overrides.error()});
764783

765784
// [log] section — re-initialize logger with config values
766785
{

‎tests/unit/test_sources.cpp‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import std;
88
import mcpp.manifest;
9+
import mcpp.config;
10+
import mcpp.libs.toml;
911
import mcpp.build.directives;
1012
import mcpp.toolchain.dialect;
1113
import mcpp.toolchain.registry;
@@ -230,6 +232,47 @@ TEST(Sources, OverrideVersionIsCheckedAgainstRequirements) {
230232
EXPECT_EQ(reqs[0], ">=3.31 by mcpp:plugins");
231233
}
232234

235+
// ── [xlings.overrides] in config.toml ───────────────────────────────────────
236+
237+
TEST(Sources, ConfigOverridesParseWithTheManifestsTwoShapes) {
238+
auto doc = mcpp::libs::toml::parse(R"(
239+
[xlings.overrides]
240+
"xim:cmake" = "/usr/bin/cmake"
241+
vcpkg = { root = "/opt/vcpkg" }
242+
"xim:slang" = { program = "slangc", version = "2026.14.1" }
243+
)");
244+
ASSERT_TRUE(doc.has_value());
245+
auto o = mcpp::config::parse_payload_overrides(*doc);
246+
ASSERT_TRUE(o.has_value()) << o.error();
247+
ASSERT_EQ(o->size(), 3u);
248+
EXPECT_EQ(o->at("xim:cmake").kind, "path");
249+
EXPECT_EQ(o->at("xim:cmake").value, "/usr/bin/cmake");
250+
// A key without a namespace names the `xim` package, as everywhere else.
251+
EXPECT_EQ(o->at("xim:vcpkg").kind, "root");
252+
EXPECT_EQ(o->at("xim:slang").kind, "program");
253+
EXPECT_EQ(o->at("xim:slang").version, "2026.14.1");
254+
EXPECT_GT(o->at("xim:cmake").line, 0);
255+
}
256+
257+
TEST(Sources, ConfigOverrideWithAnUnknownKeyIsRefused) {
258+
auto doc = mcpp::libs::toml::parse(R"(
259+
[xlings.overrides]
260+
cmake = { programme = "/usr/bin/cmake" }
261+
)");
262+
ASSERT_TRUE(doc.has_value());
263+
auto o = mcpp::config::parse_payload_overrides(*doc);
264+
ASSERT_FALSE(o.has_value());
265+
EXPECT_NE(o.error().find("programme"), std::string::npos) << o.error();
266+
}
267+
268+
TEST(Sources, ConfigWithoutTheTableHasNoOverrides) {
269+
auto doc = mcpp::libs::toml::parse("[toolchain]\ndefault = \"gcc@16.1.0\"\n");
270+
ASSERT_TRUE(doc.has_value());
271+
auto o = mcpp::config::parse_payload_overrides(*doc);
272+
ASSERT_TRUE(o.has_value()) << o.error();
273+
EXPECT_TRUE(o->empty());
274+
}
275+
233276
// ── A toolchain named by path ───────────────────────────────────────────────
234277

235278
TEST(Sources, PathSpecTakesTheFamilyFromTheDrivers) {

0 commit comments

Comments
 (0)