diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9b5a27..4fb7b47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -616,7 +616,8 @@ jobs: - name: The Marketplace submission would name this verified plugin run: | set -euo pipefail - version="$(sed -n 's/^version *= *"\(.*\)"/\1/p' mcpp.toml | head -1)" + # The plugin's own version: a component's patch moves it apart from the backend's (mcpp.toml). + version="$(sed -n 's/^pluginVersion *= *//p' mide-plugins/clion/gradle.properties | head -1)" archive="mide-plugins/clion/build/distributions/mcpp-clion-$version.zip" test -f "$archive" digest="$(sha256sum "$archive" | cut -d ' ' -f1)" diff --git a/CHANGELOG.md b/CHANGELOG.md index 60a4f69..7df5826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ The changelog is kept by series (`X.Y`); each section lists its components' rele ## 1.2 +### backend 1.2.1 + +- **mtest's cases as mtest reads them.** The cases of a file of mtest are read with mtest's own + static manifest (`mcpplibs.mtest.manifest` 0.4.1) instead of a reader of the backend's: an object + (`UTest parse{"parse", [] { ... }}`, and `Suite`, `CTS`, `E2E`, `Bench` or a suite type derived from + one) is a case, and so is each `"name"_test` it declares, under its name. A suite type the project's + tests declare in a module beside them (`class Fixture : public CTS` in `tests/cts/cts.cppm`) is read + from `tests/` and holds in every file that uses it. A file written for mtest 0.3 or 0.4 shows its + cases before it is built, as one written for 0.2 did. + ### 1.2.0 The first series of one version for every component: the backend, mcpp-vscode, mcpp-clion and diff --git a/mcpp.lock b/mcpp.lock index 7cced65..6b1ee90 100644 --- a/mcpp.lock +++ b/mcpp.lock @@ -81,6 +81,12 @@ source = "index+mcpplibs@0.4.0" hash = "fnv1a:c06e4aa4f73f1f27" targets = ["aarch64-openkal", "x86_64-linux-gnu", "x86_64-openkal"] +[package."mcpplibs.mtest.manifest"] +namespace = "mcpplibs.mtest" +version = "0.4.1" +source = "index+mcpplibs.mtest@0.4.1" +hash = "fnv1a:0339d37dd3c40581" + [package."nlohmann.json"] namespace = "nlohmann" version = "3.12.0" diff --git a/mcpp.toml b/mcpp.toml index f8075dc..8627ffd 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -9,7 +9,7 @@ # M2/M3), where the system it runs upon is answered when the image is loaded. [package] name = "mcpp-ide" -version = "1.2.0" +version = "1.2.1" description = "The IDE backend for mcpp: sessions, jobs, records and view models over mcpp's control contract" license = "Apache-2.0" authors = ["mcpp-community"] @@ -80,6 +80,8 @@ main = "src/bin/dev.cpp" # runtime and loader remain the platform's. mcpplibs.os = "0.4.0" nlohmann.json = "3.12.0" +# mtest's static case manifest: the cases a test file declares, read as mtest reads them. +mcpplibs.mtest.manifest = "0.4.1" # What every backend test shares (testsupport/): a module, as the backend's units are. [dev-dependencies] diff --git a/src/scan.cppm b/src/scan.cppm index 0d0cc41..b3947b4 100644 --- a/src/scan.cppm +++ b/src/scan.cppm @@ -13,6 +13,7 @@ export module mcppide.scan; import std; +import mcpplibs.mtest.manifest; export namespace mcppide::scan { @@ -25,8 +26,10 @@ struct Call { std::vector arguments; }; -// A case of the mtest library (`"name"_test`): its identifier -- the names of the suites it is -// declared in and its own, joined by `/` -- and where its name is (0-based). +// A case of the mtest library -- an object (`UTest parse{"parse", ...}`, any suite type) or a +// `"name"_test` declared in one -- as mtest's own static manifest reads it +// (mcpplibs.mtest.manifest): its identifier, the names of what declares it and its own joined by +// `/`, and where its name is (0-based). struct Literal { std::string id; int line = 0; @@ -36,11 +39,13 @@ struct Literal { struct Result { std::optional mainLine; // 0-based std::vector calls; - std::vector cases; // `"name"_test`, in `suite x{"name", ...}` scopes + std::vector cases; // mtest's cases, by mcpplibs.mtest.manifest }; -// Scans `text` for `main` and for calls of the macros named in `macros`. -Result scan(std::string_view text, const std::vector& macros); +// Scans `text` for `main` and for calls of the macros named in `macros`. `types` are the suite types +// the project's tests declare elsewhere (`class Fixture : public CTS` in a module beside them). +Result scan(std::string_view text, const std::vector& macros, + const mcpplibs::mtest::manifest::suite_types& types = {}); // A string literal's contents, as the program sees them for the plain escapes (\" \\ \n \t); the // argument itself when it is not one literal. Adjacent literals are joined. @@ -157,24 +162,14 @@ std::vector arguments(Cursor& c) { } // namespace -Result scan(std::string_view text, const std::vector& macros) { +Result scan(std::string_view text, const std::vector& macros, + const mcpplibs::mtest::manifest::suite_types& types) { Result r; Cursor c{text}; int depth = 0; // braces bool atDeclaration = true; // the next identifier starts a declaration bool lineHasCode = false; // a `#` is a directive only first on its line std::string previous; // the previous identifier, for `int main(` - // A suite of the mtest library: `suite {"", ...}`, whose `{` opens the scope its - // cases' identifiers are prefixed in. `suiteState` follows the declaration: 1 after `suite`, - // 2 after its variable's name, 3 after the `{` (the next literal is the suite's identifier). - struct Scope { std::string name; int depth; }; - std::vector suites; - int suiteState = 0, suiteDepth = 0; - auto prefix = [&] { - std::string out; - for (const auto& s : suites) if (!s.name.empty()) out += s.name + "/"; - return out; - }; while (!c.done()) { const char ch = c.peek(); if (ch == '\n') { lineHasCode = false; c.step(); continue; } @@ -185,16 +180,7 @@ Result scan(std::string_view text, const std::vector& macros) { lineHasCode = true; if (c.on_raw()) { c.skip_raw(); atDeclaration = false; previous.clear(); continue; } if (ch == '"') { - const auto from = c.at; - const int line = c.line, column = c.column(); c.skip_quoted(ch); - const auto value = literal_value(text.substr(from, c.at - from)); - if (suiteState == 3) { - suites.push_back({value, suiteDepth}); - suiteState = 0; - } else if (text.substr(c.at, 5) == "_test" && !(c.at + 5 < text.size() && ident_char(text[c.at + 5]))) { - r.cases.push_back(Literal{prefix() + value, line, column}); - } atDeclaration = false; previous.clear(); continue; @@ -214,10 +200,6 @@ Result scan(std::string_view text, const std::vector& macros) { break; } const bool call = look.peek() == '('; - // `suite`, or `mtest::suite` (the identifier before `::` is kept in `previous`). - if (word == "suite" && (atDeclaration || previous == "mtest")) suiteState = 1; - else if (suiteState == 1) suiteState = 2; - else suiteState = 0; if (call && atDeclaration && std::ranges::find(macros, word) != macros.end()) { c = look; r.calls.push_back(Call{word, line, column, arguments(c)}); @@ -232,21 +214,21 @@ Result scan(std::string_view text, const std::vector& macros) { continue; } if (ch == '{') { - if (suiteState == 2) { suiteState = 3; suiteDepth = depth; } - else suiteState = 0; ++depth; atDeclaration = true; } else if (ch == '}') { if (depth > 0) --depth; - while (!suites.empty() && suites.back().depth >= depth) suites.pop_back(); atDeclaration = true; } - else if (ch == ';') { atDeclaration = true; suiteState = 0; } - else { atDeclaration = false; suiteState = 0; } + else if (ch == ';') atDeclaration = true; + else atDeclaration = false; // `std::main`, `x.main`: not the program's. if (ch != ':' ) previous.clear(); c.step(); } + // mtest's cases as mtest reads them: its manifest is the one reader of its declarations. + for (const auto& found : mcpplibs::mtest::manifest::scan(text, types)) + r.cases.push_back(Literal{found.id, found.line - 1, found.column - 1}); return r; } diff --git a/src/server.cppm b/src/server.cppm index c0912c3..ddd97c4 100644 --- a/src/server.cppm +++ b/src/server.cppm @@ -26,6 +26,7 @@ import mcppide.documents; import mcppide.spec; import mcppide.languageservice; import mcppide.scan; +import mcpplibs.mtest.manifest; import mcppide.testing; import mcpplibs.os; @@ -33,7 +34,7 @@ export namespace mcppide::server { using Json = nlohmann::json; -inline constexpr std::string_view kVersion = "1.2.0"; +inline constexpr std::string_view kVersion = "1.2.1"; // THE SERIES (mcpp design 2026-11-07, D15): `X.Y` of `X.Y.Z`, shared by the backend and every editor // plugin; components of one series work together, and `Z` is each component's own patch. A plugin // refuses a backend of another series, naming both versions. @@ -1470,6 +1471,18 @@ std::optional test_name_of(const std::filesystem::path& root, const return below.generic_string(); } +// The suite types the project's tests declare (`class Fixture : public CTS` in a module beside them), +// read across the files of tests/ as mtest's static manifest reads them. +mcpplibs::mtest::manifest::suite_types test_suite_types(const std::filesystem::path& root) { + static const std::set cxx{".cpp", ".cc", ".cxx", ".cppm", ".ixx", ".mpp"}; + std::vector sources; + std::error_code ec; + for (std::filesystem::recursive_directory_iterator it(root / "tests", ec), end; !ec && it != end; it.increment(ec)) + if (it->is_regular_file(ec) && cxx.contains(it->path().extension().string())) + sources.push_back(read_text(it->path())); + return mcpplibs::mtest::manifest::types_of(sources); +} + // The source of a test by its name: tests/.cpp (or .cc, .cxx). std::filesystem::path test_source(const std::filesystem::path& root, std::string_view name) { for (const auto* ext : {".cpp", ".cc", ".cxx"}) { @@ -1492,7 +1505,7 @@ std::expected Session::source_entries(const Json& p) { if (!proj) return Json{{"file", file.generic_string()}, {"kind", "none"}}; const auto text = p.contains("text") && p["text"].is_string() ? p["text"].get() : read_text(file); const auto extra = test_macros(); - const auto found = scan::scan(text, testing::macros(extra)); + const auto found = scan::scan(text, testing::macros(extra), test_suite_types(proj->root)); Json out{{"file", file.generic_string()}, {"root", proj->root.generic_string()}, {"kind", "none"}}; if (auto name = test_name_of(proj->root, file)) { out["kind"] = "test"; @@ -1520,7 +1533,7 @@ std::expected Session::test_cases(const Json& p) { Json out{{"root", root.generic_string()}, {"test", test}, {"file", file.generic_string()}, {"framework", nullptr}, {"cases", Json::array()}}; if (file.empty()) return out; const auto extra = test_macros(); - const auto found = scan::scan(read_text(file), testing::macros(extra)); + const auto found = scan::scan(read_text(file), testing::macros(extra), test_suite_types(root)); if (found.mainLine) out["main"] = Json{{"line", *found.mainLine}, {"approximate", true}}; bool trusted; { diff --git a/tests/test_testing.cpp b/tests/test_testing.cpp index e23ae4d..9ff6898 100644 --- a/tests/test_testing.cpp +++ b/tests/test_testing.cpp @@ -7,6 +7,7 @@ import std; import mcppide.testsupport; import nlohmann.json; import mcppide.scan; +import mcpplibs.mtest.manifest; import mcppide.testing; import mcppide.rpc; import mcppide.server; @@ -149,31 +150,30 @@ TEST(Math, check(testing::case_outputs(fw("catch2"), "[ RUN ] x\n").empty()); } - // mtest's cases: `"name"_test` literals, prefixed by the suites they are declared in; never a - // literal in a comment or one not followed by `_test`. The framework is claimed by its package - // and its cases, and a record of `mcpp test` lists and reports them (the test protocol). + // mtest's cases as mtest's static manifest reads them: each object (`UTest`, `Suite`, ...) is a + // case, and so is each `"name"_test` declared in one, under its name; never a literal in a + // comment or one not followed by `_test`. The framework is claimed by its package and its + // cases, and a record of `mcpp test` lists and reports them (the test protocol). { const auto r = scan::scan(R"(import mcpplibs.mtest; using namespace mcpplibs::mtest; // "commented"_test -suite parsing{"parsing", [] { - "an empty line"_test = [] { expect(true); }; - suite fields{"fields", [] { - "one field"_test[tag{"x"}] = [] {}; - }}; +UTest empty{"an empty line", [] { expect(true); }}; +Suite parsing{"parsing", tag{"p"}, [] { + "one field"_test[tag{"x"}] = [] {}; const char* s = "not a case"; }}; -mcpplibs::mtest::suite other{"other", [] { "after"_test = [] {}; }}; +mcpplibs::mtest::E2E other{"other", [] { "after"_test = [] {}; }}; int main() { "top"_test = [] {}; return report(); } )", {}); - check(r.cases.size() == 4); - check(r.cases.size() == 4 && r.cases[0].id == "parsing/an empty line" && r.cases[0].line == 4); - check(r.cases.size() == 4 && r.cases[1].id == "parsing/fields/one field"); - check(r.cases.size() == 4 && r.cases[2].id == "other/after" && r.cases[3].id == "top"); + std::vector ids; + for (const auto& c : r.cases) ids.push_back(c.id); + check(ids == std::vector{"an empty line", "parsing", "parsing/one field", "other", "other/after", "top"}); + check(r.cases.size() == 6 && r.cases[0].line == 3 && r.cases[2].line == 5); check(testing::framework_of({"app", "mcpplibs.mtest"}, r, Json::object()) == &fw("mtest")); check(testing::framework_of({"app"}, r, Json::object()) == nullptr); const auto cases = testing::static_cases(fw("mtest"), r, Json::object()); - check(cases.size() == 4 && cases[0].name == "parsing/an empty line" && cases[0].line == 4); + check(cases.size() == 6 && cases[0].name == "an empty line" && cases[0].line == 3); const auto record = Json::parse(R"({"test":"parse","protocol":1, "listed":[{"id":"parsing/an empty line","file":"tests/parse.cpp","line":5}], @@ -192,6 +192,19 @@ int main() { "top"_test = [] {}; return report(); } check(outcomes.size() == 4 && outcomes[3].outcome.status == "passed"); } + // A suite type the project's tests declare in a module beside them is a suite type in every + // file that uses it: the server reads the types across tests/ before it scans a file. + { + const std::vector sources{ + "export module cts;\nimport mcpplibs.mtest;\nexport class Fixture : public mcpplibs::mtest::CTS {\n" + "public:\n using CTS::CTS;\n};\n"}; + const auto types = mcpplibs::mtest::manifest::types_of(sources); + const std::string_view cases = "import cts;\nFixture one{\"one\"};\nFixture two{\"two@warm\"};\n"; + const auto r = scan::scan(cases, {}, types); + check(r.cases.size() == 2 && r.cases[0].id == "one" && r.cases[1].id == "two@warm" && r.cases[1].line == 2); + check(scan::scan(cases, {}).cases.empty()); + } + // source/entries and test/cases over a project on disk (no mcpp: the plan's packages are not // known, so no framework is claimed, and the static entries still answer). {