#include import cargoxx.manifest; import cargoxx.util; import std; using cargoxx::manifest::Edition; using cargoxx::manifest::parse; using cargoxx::util::ErrorCode; namespace { auto write_manifest(std::string_view body) -> std::filesystem::path { auto dir = std::filesystem::temp_directory_path() / std::format("cargoxx-manifest-test-{}", std::random_device{}()); std::filesystem::create_directories(dir); auto path = dir / "Cargoxx.toml"; std::ofstream{path} << body; return path; } } // namespace TEST_CASE("parse rejects missing file", "[manifest][parse]") { auto r = parse("/nonexistent/Cargoxx.toml"); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestNotFound); } TEST_CASE("parse rejects malformed toml", "[manifest][parse]") { auto p = write_manifest("[package\nname = "); auto r = parse(p); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestParseError); } TEST_CASE("parse accepts a minimal manifest", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" )"); auto r = parse(p); REQUIRE(r.has_value()); REQUIRE(r->package.name == "foo"); REQUIRE(r->package.version == "0.1.0"); REQUIRE(r->package.edition == Edition::Cpp23); REQUIRE(r->dependencies.empty()); REQUIRE_FALSE(r->build.warnings_as_errors); } TEST_CASE("parse handles all edition values", "[manifest][parse]") { for (auto [s, e] : std::vector>{ {"cpp20", Edition::Cpp20}, {"cpp23", Edition::Cpp23}, {"cpp26", Edition::Cpp26}}) { auto p = write_manifest(std::format(R"( [package] name = "foo" version = "0.1.0" edition = "{}" )", s)); auto r = parse(p); REQUIRE(r.has_value()); REQUIRE(r->package.edition == e); } } TEST_CASE("parse rejects unknown edition", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" edition = "cpp99" )"); auto r = parse(p); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestInvalidField); } TEST_CASE("parse extracts authors and license", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" authors = ["Ada", "Grace"] license = "MIT" )"); auto r = parse(p); REQUIRE(r.has_value()); REQUIRE(r->package.authors == std::vector{"Ada", "Grace"}); REQUIRE(r->package.license == "MIT"); } TEST_CASE("parse accepts string-form dependency", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" [dependencies] fmt = "10.2" )"); auto r = parse(p); REQUIRE(r.has_value()); REQUIRE(r->dependencies.size() == 1); REQUIRE(r->dependencies[0].name == "fmt"); REQUIRE(r->dependencies[0].version_spec == "10.2"); REQUIRE(r->dependencies[0].components.empty()); } TEST_CASE("parse accepts table-form dependency with components", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" [dependencies] boost = { version = "1.84", components = ["filesystem", "system"] } )"); auto r = parse(p); REQUIRE(r.has_value()); REQUIRE(r->dependencies.size() == 1); REQUIRE(r->dependencies[0].name == "boost"); REQUIRE(r->dependencies[0].version_spec == "1.84"); REQUIRE(r->dependencies[0].components == std::vector{"filesystem", "system"}); } TEST_CASE("parse rejects dependency value of wrong type", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" [dependencies] weird = 42 )"); auto r = parse(p); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestInvalidField); } TEST_CASE("parse extracts build settings", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" [build] warnings_as_errors = true sanitizers = ["address", "undefined"] )"); auto r = parse(p); REQUIRE(r.has_value()); REQUIRE(r->build.warnings_as_errors); REQUIRE(r->build.sanitizers == std::vector{"address", "undefined"}); } TEST_CASE("parse rejects unknown [package] key", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" flavor = "spicy" )"); auto r = parse(p); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestUnknownField); } TEST_CASE("parse rejects unknown [build] key", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" [build] optimize = "max" )"); auto r = parse(p); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestUnknownField); } TEST_CASE("parse accepts reserved [package] fields", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" description = "demo" repository = "https://example.com/foo" )"); auto r = parse(p); REQUIRE(r.has_value()); } TEST_CASE("parse accepts reserved top-level tables", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" [features] default = [] [workspace] members = ["a"] )"); auto r = parse(p); REQUIRE(r.has_value()); } TEST_CASE("parse extracts dev-dependencies", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" [dev-dependencies] catch2 = "3.5.0" gtest = { version = "1.14", components = ["gmock"] } )"); auto r = parse(p); REQUIRE(r.has_value()); REQUIRE(r->dev_dependencies.size() == 2); auto find = [&](std::string_view n) { return std::ranges::find_if(r->dev_dependencies, [&](const auto& d) { return d.name == n; }); }; auto catch2 = find("catch2"); REQUIRE(catch2 != r->dev_dependencies.end()); REQUIRE(catch2->version_spec == "3.5.0"); REQUIRE(catch2->components.empty()); auto gtest = find("gtest"); REQUIRE(gtest != r->dev_dependencies.end()); REQUIRE(gtest->version_spec == "1.14"); REQUIRE(gtest->components == std::vector{"gmock"}); } TEST_CASE("parse extracts [build].include_dirs", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" [build] include_dirs = ["third_party", "vendor/spdlog"] )"); auto r = parse(p); REQUIRE(r.has_value()); REQUIRE(r->build.include_dirs == std::vector{"third_party", "vendor/spdlog"}); } TEST_CASE("parse rejects [build].include_dirs that escape the project", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo" version = "0.1.0" [build] include_dirs = ["../escape"] )"); auto r = parse(p); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == cargoxx::util::ErrorCode::ManifestInvalidField); } TEST_CASE("parse rejects invalid name with spaces", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "foo bar" version = "0.1.0" )"); auto r = parse(p); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::LayoutInvalidName); } TEST_CASE("parse rejects name starting with digit", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "1foo" version = "0.1.0" )"); auto r = parse(p); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::LayoutInvalidName); } TEST_CASE("parse rejects missing [package].name", "[manifest][parse]") { auto p = write_manifest(R"( [package] version = "0.1.0" )"); auto r = parse(p); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestInvalidField); } TEST_CASE("parse recognizes { path = \"...\" } as a cargoxx path dep", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "consumer" version = "0.1.0" edition = "cpp23" [dependencies] mylib = { path = "../mylib" } )"); auto r = parse(p); REQUIRE(r.has_value()); REQUIRE(r->dependencies.size() == 1); const auto& dep = r->dependencies[0]; REQUIRE(dep.name == "mylib"); REQUIRE(dep.source == cargoxx::manifest::DepSource::CargoxxPath); REQUIRE(dep.path.has_value()); REQUIRE(*dep.path == "../mylib"); // Version defaults to "*" when only `path` is given. REQUIRE(dep.version_spec == "*"); } TEST_CASE("parse rejects dep table without version or path", "[manifest][parse]") { auto p = write_manifest(R"( [package] name = "consumer" version = "0.1.0" edition = "cpp23" [dependencies] mylib = { components = ["a"] } )"); auto r = parse(p); REQUIRE_FALSE(r.has_value()); REQUIRE(r.error().code == ErrorCode::ManifestInvalidField); }