Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions mcpp.lock
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ source = "[email protected]"
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 = "[email protected]"
hash = "fnv1a:0339d37dd3c40581"

[package."nlohmann.json"]
namespace = "nlohmann"
version = "3.12.0"
Expand Down
4 changes: 3 additions & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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]
Expand Down
52 changes: 17 additions & 35 deletions src/scan.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
export module mcppide.scan;

import std;
import mcpplibs.mtest.manifest;

export namespace mcppide::scan {

Expand All @@ -25,8 +26,10 @@ struct Call {
std::vector<std::string> 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;
Expand All @@ -36,11 +39,13 @@ struct Literal {
struct Result {
std::optional<int> mainLine; // 0-based
std::vector<Call> calls;
std::vector<Literal> cases; // `"name"_test`, in `suite x{"name", ...}` scopes
std::vector<Literal> 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<std::string>& 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<std::string>& 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.
Expand Down Expand Up @@ -157,24 +162,14 @@ std::vector<std::string> arguments(Cursor& c) {

} // namespace

Result scan(std::string_view text, const std::vector<std::string>& macros) {
Result scan(std::string_view text, const std::vector<std::string>& 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 <name>{"<id>", ...}`, 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<Scope> 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; }
Expand All @@ -185,16 +180,7 @@ Result scan(std::string_view text, const std::vector<std::string>& 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;
Expand All @@ -214,10 +200,6 @@ Result scan(std::string_view text, const std::vector<std::string>& 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)});
Expand All @@ -232,21 +214,21 @@ Result scan(std::string_view text, const std::vector<std::string>& 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;
}

Expand Down
19 changes: 16 additions & 3 deletions src/server.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ import mcppide.documents;
import mcppide.spec;
import mcppide.languageservice;
import mcppide.scan;
import mcpplibs.mtest.manifest;
import mcppide.testing;
import mcpplibs.os;

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.
Expand Down Expand Up @@ -1470,6 +1471,18 @@ std::optional<std::string> 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<std::string> cxx{".cpp", ".cc", ".cxx", ".cppm", ".ixx", ".mpp"};
std::vector<std::string> 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/<name>.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"}) {
Expand All @@ -1492,7 +1505,7 @@ std::expected<Json, rpc::Error> 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<std::string>() : 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";
Expand Down Expand Up @@ -1520,7 +1533,7 @@ std::expected<Json, rpc::Error> 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;
{
Expand Down
41 changes: 27 additions & 14 deletions tests/test_testing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<std::string> ids;
for (const auto& c : r.cases) ids.push_back(c.id);
check(ids == std::vector<std::string>{"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}],
Expand All @@ -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<std::string> 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).
{
Expand Down
Loading