From d35cf0f136e74bc62f8a392309a7089ec16cf18c Mon Sep 17 00:00:00 2001 From: Iuri Matias Date: Tue, 14 Apr 2026 10:38:58 -0400 Subject: [PATCH] add test framework to scaffolded module --- docs/spec.md | 12 +- guidelines/testing.md | 145 +++++++++---- mcp-server/tools/build-help.ts | 23 ++- mcp-server/tools/scaffold.ts | 54 +++-- skills/create-universal-module/SKILL.md | 64 +++++- skills/testing-modules/SKILL.md | 261 ++++++++++++++++-------- templates/universal-module/README.md | 24 ++- tests/run-scaffold-tests.sh | 10 + 8 files changed, 445 insertions(+), 148 deletions(-) diff --git a/docs/spec.md b/docs/spec.md index 082abbf..01303fe 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -124,7 +124,9 @@ crypto_utils/ ├── CMakeLists.txt # logos_module() with generated_code sources ├── flake.nix # preConfigure runs logos-cpp-generator --from-header ├── tests/ -│ └── test_crypto_utils.cpp # Unit tests against impl class directly +│ ├── main.cpp # LOGOS_TEST_MAIN() entry point +│ ├── test_crypto_utils.cpp # Unit tests using LOGOS_TEST() and assertions +│ └── CMakeLists.txt # logos_test() macro (auto-detected by builder) ├── CLAUDE.md # Generated: knows this is a universal module ├── AGENTS.md # Universal context for any AI tool └── .mcp.json # MCP server registration @@ -178,10 +180,10 @@ logoscore -m ./result/lib -l crypto_utils \ **Step 5: Unit test (no logoscore needed)** ```bash -nix flake check -L +nix build .#unit-tests -L ``` -Unit tests instantiate `CryptoUtilsImpl` directly — it is a plain C++ class with no framework dependencies. +Unit tests use logos-test-framework (`LOGOS_TEST()` macros, `LOGOS_ASSERT_*`) and instantiate `CryptoUtilsImpl` directly — it is a plain C++ class with no framework dependencies. `logos-module-builder` auto-detects `tests/CMakeLists.txt` and creates the `unit-tests` target. **Step 6: Inter-module communication** @@ -208,7 +210,7 @@ lgx verify crypto_utils.lgx - Step 1: `init` command scaffolds from universal module template; generated CLAUDE.md/AGENTS.md teach agents the universal pattern - Step 2: Guidelines ensure pure C++, no Qt types; the type mapping table is always available - Step 3: Build help explains the codegen pipeline; troubleshooting for common generator errors -- Steps 4-5: Testing skill covers logoscore integration tests and direct unit tests +- Steps 4-5: Testing skill covers logos-test-framework unit tests (LOGOS_TEST, LogosTestContext, mocking) and logoscore integration tests - Step 6: Inter-module comm skill explains LogosAPI patterns and dependency declaration - Step 7: Packaging skill covers the full LGX workflow @@ -392,7 +394,7 @@ What happens when a developer tells an AI agent "create a module that provides e 5. Agent builds with `nix build` — build-help guidelines explain the pipeline. If errors occur, agent knows common fixes: generator type mapping issues, missing `find_package`, `metadata.json`/header class name mismatch. -6. Agent tests with `logoscore` — testing skill provides exact commands and expected output patterns. +6. Agent runs unit tests with `nix build .#unit-tests -L` — the scaffolded `tests/` directory uses logos-test-framework (`LOGOS_TEST()`, `LOGOS_ASSERT_*`). Tests are auto-detected by `logos-module-builder`. Agent also tests with `logoscore` for integration testing — testing skill provides exact commands and expected output patterns. **Without logos-dev-boost:** Agent would write `Q_INVOKABLE` methods, use `QString` everywhere, try `cmake --build` instead of `nix build`, hallucinate a `LogosPlugin` base class that doesn't exist, and have no idea about the code generator pipeline. diff --git a/guidelines/testing.md b/guidelines/testing.md index 9831f4c..300a3e3 100644 --- a/guidelines/testing.md +++ b/guidelines/testing.md @@ -1,63 +1,136 @@ # Testing Logos Modules -## Two Testing Approaches +## Unit Tests with logos-test-framework -### 1. Unit Tests (Universal Modules) +Universal modules have a plain C++ impl class. Test it using the logos-test-framework, which is provided automatically by `logos-module-builder`. -Universal modules have a plain C++ impl class with no framework dependencies. Test it directly: +### Test File Structure -```cpp -#include "my_module_impl.h" -#include - -int main() { - MyModuleImpl impl; - assert(impl.hash("hello") == "expected_hash"); - assert(impl.verify("hello", "expected_hash") == true); - return 0; -} +``` +tests/ +├── main.cpp # LOGOS_TEST_MAIN() entry point +├── test_my_module.cpp # Test cases using LOGOS_TEST() +└── CMakeLists.txt # logos_test() macro ``` -For the SDK test framework, use `LOGOS_TEST_MAIN()`: +### Writing Tests ```cpp -#include "my_module_impl.h" -#include "logos_test.h" - +// tests/main.cpp +#include LOGOS_TEST_MAIN() +``` -TEST(MyModule, HashWorks) { +```cpp +// tests/test_my_module.cpp +#include +#include "../src/my_module_impl.h" + +LOGOS_TEST(hash_returns_nonempty_string) { MyModuleImpl impl; - EXPECT_FALSE(impl.hash("hello").empty()); + LOGOS_ASSERT_FALSE(impl.hash("hello").empty()); +} + +LOGOS_TEST(verify_matches_hash) { + MyModuleImpl impl; + auto hash = impl.hash("hello"); + LOGOS_ASSERT_TRUE(impl.verify("hello", hash)); } ``` -Add unit tests to the flake by including a `checks` output or a `tests/` directory with its own `CMakeLists.txt`. +### CMakeLists.txt for Tests -### 2. Integration Tests with logoscore +```cmake +# tests/CMakeLists.txt +cmake_minimum_required(VERSION 3.14) +project(MyModuleTests LANGUAGES CXX) + +include(LogosTest) + +logos_test( + NAME my_module_tests + MODULE_SOURCES ../src/my_module_impl.cpp + TEST_SOURCES + main.cpp + test_my_module.cpp +) +``` + +The `logos_test()` CMake macro handles all framework wiring: Qt dependencies, SDK mock headers, include paths, and CTest registration. + +### Mocking Other Modules + +Use `LogosTestContext` when your module calls other modules: + +```cpp +LOGOS_TEST(calls_waku_publish) { + auto t = LogosTestContext("chat_module"); + t.mockModule("waku_module", "relayPublish").returns(true); + + ChatImpl impl; + t.init(&impl); + + impl.sendMessage("hello"); + LOGOS_ASSERT(t.moduleCalled("waku_module", "relayPublish")); +} +``` + +### Mocking C Libraries + +For modules wrapping external C/C++ libraries, write mock stubs: + +```cpp +// tests/mocks/mock_libcalc.cpp +#include +extern "C" { #include "libcalc.h" } + +extern "C" int calc_add(int a, int b) { + LOGOS_CMOCK_RECORD("calc_add"); + return LOGOS_CMOCK_RETURN(int, "calc_add"); +} +``` + +Reference them in CMake: + +```cmake +logos_test( + NAME calc_module_tests + MODULE_SOURCES ../src/calc_module_impl.cpp + TEST_SOURCES main.cpp test_calc.cpp + MOCK_C_SOURCES mocks/mock_libcalc.cpp +) +``` + +### Running Unit Tests + +```bash +nix build .#unit-tests -L # Build and run unit tests +nix flake check -L # All Nix checks including tests +``` + +`logos-module-builder` auto-detects `tests/CMakeLists.txt` and adds `checks..unit-tests` and `packages..unit-tests` automatically. + +## Integration Tests with logoscore Test the module as a loaded plugin via the headless runtime: ```bash -# Load module and call a method logoscore -m ./result/lib -l my_module \ -c "my_module.doSomething(test_input)" -# Multiple sequential calls logoscore -m ./result/lib -l my_module \ -c "my_module.init(config)" \ -c "my_module.process(data)" -# Load multiple modules (deps resolved automatically) logoscore -m ./result/lib -l my_module,other_module \ -c "my_module.callOther(hello)" ``` logoscore arguments: -- `-m ` — Directory to scan for module plugins (repeatable) -- `-l ` — Comma-separated modules to load -- `-c ".(args)"` — Call a method (repeatable, sequential) -- `--quit-on-finish` — Exit after calls complete (for CI) +- `-m ` -- Directory to scan for module plugins (repeatable) +- `-l ` -- Comma-separated modules to load +- `-c ".(args)"` -- Call a method (repeatable, sequential) +- `--quit-on-finish` -- Exit after calls complete (for CI) Type auto-detection in `-c` args: `true`/`false` -> bool, `42` -> int, `3.14` -> double, else -> string. Use `@filename` to load file content as an argument. @@ -74,18 +147,20 @@ TEST_GROUPS=basic,ipc,errors ws test logos-test-modules --auto-local ### Running Tests via Nix ```bash -# Run all checks defined in the flake -nix flake check -L +nix build .#unit-tests -L # Run unit tests +nix flake check -L # Run all checks defined in the flake -# In the workspace -ws test my-module -ws test my-module --auto-local # with local dep overrides -ws test --all --type cpp # all C++ repos +ws test my-module # In the workspace +ws test my-module --auto-local # With local dep overrides +ws test --all --type cpp # All C++ repos ``` ## Key Testing Rules -- Unit tests should NOT require logoscore — instantiate the impl class directly +- Unit tests use `LOGOS_TEST()` and `LOGOS_ASSERT_*` macros from `` +- Unit tests should NOT require logoscore -- instantiate the impl class directly +- `tests/CMakeLists.txt` must use `include(LogosTest)` + `logos_test()` +- Use `LogosTestContext` for mocking module calls and C library functions - Integration tests verify the full plugin lifecycle (load, call, response) - Always test with `--quit-on-finish` in CI to ensure the process exits - 30-second timeout per `-c` call; exit code 1 on failure diff --git a/mcp-server/tools/build-help.ts b/mcp-server/tools/build-help.ts index 99a2a17..3c6cdc2 100644 --- a/mcp-server/tools/build-help.ts +++ b/mcp-server/tools/build-help.ts @@ -116,13 +116,24 @@ function buildHelp(project: { isUniversal: boolean; name: string; hasFlake: bool function testHelp(project: { isUniversal: boolean; name: string }): string { const lines = ["## Test Commands\n"]; + const pascal = project.name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(""); if (project.isUniversal) { - lines.push("### Unit Tests (direct impl class testing)\n"); + lines.push("### Unit Tests (logos-test-framework)\n"); lines.push("```bash"); - lines.push("nix flake check -L # Run Nix-defined checks"); + lines.push("nix build .#unit-tests -L # Build and run unit tests"); + lines.push("nix flake check -L # Run all Nix checks including tests"); + lines.push("```\n"); + lines.push(`Unit tests instantiate \`${pascal}Impl\` directly — no logoscore needed.\n`); + lines.push("Tests use `LOGOS_TEST()` macros and `LogosTestContext` for mocking."); + lines.push("Test files live in `tests/` with `CMakeLists.txt` using `logos_test()`."); + lines.push("`logos-module-builder` auto-detects `tests/CMakeLists.txt` and creates the `unit-tests` target.\n"); + lines.push("### Test Runner CLI\n"); + lines.push("```bash"); + lines.push(`./${project.name}_tests --filter # Run matching tests only`); + lines.push(`./${project.name}_tests --json # JSON output for CI/agents`); + lines.push(`./${project.name}_tests --no-color # Disable colored output`); lines.push("```\n"); - lines.push("Unit tests instantiate `" + project.name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") + "Impl` directly — no logoscore needed.\n"); } lines.push("### Integration Tests with logoscore\n"); @@ -219,6 +230,10 @@ function troubleshoot(error: string, project: { isUniversal: boolean; name: stri lines.push("**Fix:** Store references in the output mean it's a dev build, not portable. Use `nix build .#portable` for distribution."); } else if (errorLower.includes("git") || errorLower.includes("not a git")) { lines.push("**Fix:** Nix flakes require git tracking. Run `git add -A` before `nix build`."); + } else if (errorLower.includes("logostest") || errorLower.includes("logos_test") || errorLower.includes("logos_test.h")) { + lines.push("**Fix:** Test framework not found. Ensure `tests/CMakeLists.txt` uses `include(LogosTest)` and that `logos-module-builder` is your flake input (it provides the test framework automatically)."); + } else if (errorLower.includes("unit-tests") || errorLower.includes("unit_tests")) { + lines.push("**Fix:** The `unit-tests` target requires `tests/CMakeLists.txt` in your project root. Create it with `include(LogosTest)` and `logos_test()`. The builder auto-detects it."); } else { lines.push("**General tips:**"); lines.push("- Build with `-L` for full logs: `nix build -L`"); @@ -247,6 +262,8 @@ function commonIssues(project: { isUniversal: boolean }): string { lines.push("6. **Generated files not found** — Check `preConfigure` runs logos-cpp-generator"); lines.push("7. **Unknown type mapped to any** — Use types from the supported mapping table"); lines.push("8. **Impl class not found** — `--impl-class` must exactly match the class name"); + lines.push("9. **unit-tests not found** — Add `tests/CMakeLists.txt` with `include(LogosTest)` + `logos_test()`"); + lines.push("10. **LogosTest.cmake not found** — Ensure you build via `nix build .#unit-tests`, not raw cmake"); } return lines.join("\n"); diff --git a/mcp-server/tools/scaffold.ts b/mcp-server/tools/scaffold.ts index 718162d..604fd9c 100644 --- a/mcp-server/tools/scaffold.ts +++ b/mcp-server/tools/scaffold.ts @@ -94,9 +94,12 @@ export function handleScaffold(args: Record) { `cd ${path.basename(projectDir)}`, "git init && git add -A", "nix build", - type === "module" - ? `logoscore -m ./result/lib -l ${name} -c "${name}.methodName(args)"` - : "cp -r result/* ~/.local/share/Logos/LogosBasecampDev/plugins/" + name + "/", + ...(type === "module" + ? [ + "nix build .#unit-tests -L", + `logoscore -m ./result/lib -l ${name} -c "${name}.methodName(args)"`, + ] + : ["cp -r result/* ~/.local/share/Logos/LogosBasecampDev/plugins/" + name + "/"]), ], }, null, @@ -244,20 +247,47 @@ logos_module( filesCreated ); + writeFile( + path.join(dir, "tests/main.cpp"), + `#include + +LOGOS_TEST_MAIN() +`, + filesCreated + ); + writeFile( path.join(dir, `tests/test_${name}.cpp`), - `#include "../src/${name}_impl.h" -#include -#include + `#include +#include "../src/${name}_impl.h" -int main() { +LOGOS_TEST(echo_returns_prefixed_input) { ${pascal}Impl impl; - - assert(impl.echo("test") == "echo: test"); - - std::cout << "All tests passed" << std::endl; - return 0; + LOGOS_ASSERT_EQ(impl.echo("hello"), std::string("echo: hello")); } + +LOGOS_TEST(echo_handles_empty_input) { + ${pascal}Impl impl; + LOGOS_ASSERT_EQ(impl.echo(""), std::string("echo: ")); +} +`, + filesCreated + ); + + writeFile( + path.join(dir, "tests/CMakeLists.txt"), + `cmake_minimum_required(VERSION 3.14) +project(${pascal}Tests LANGUAGES CXX) + +include(LogosTest) + +logos_test( + NAME ${name}_tests + MODULE_SOURCES ../src/${name}_impl.cpp + TEST_SOURCES + main.cpp + test_${name}.cpp +) `, filesCreated ); diff --git a/skills/create-universal-module/SKILL.md b/skills/create-universal-module/SKILL.md index 9f87589..bda9118 100644 --- a/skills/create-universal-module/SKILL.md +++ b/skills/create-universal-module/SKILL.md @@ -167,17 +167,65 @@ nix build Files must be tracked by git before Nix can see them. -## Step 8: Test +## Step 8: Add Unit Tests -```bash -# Integration test via logoscore -logoscore -m ./result/lib -l -c ".exampleMethod(test)" +The scaffold creates test files using logos-test-framework. If adding tests manually: -# Unit test (if tests/ directory exists) -nix flake check -L +``` +tests/ +├── main.cpp # Test runner entry point +├── test_.cpp # Test cases +└── CMakeLists.txt # logos_test() integration ``` -## Step 9: Inspect +```cpp +// tests/main.cpp +#include +LOGOS_TEST_MAIN() +``` + +```cpp +// tests/test_.cpp +#include +#include "../src/_impl.h" + +LOGOS_TEST(example_method_works) { + impl; + LOGOS_ASSERT_FALSE(impl.exampleMethod("test").empty()); +} +``` + +```cmake +# tests/CMakeLists.txt +cmake_minimum_required(VERSION 3.14) +project(Tests LANGUAGES CXX) + +include(LogosTest) + +logos_test( + NAME _tests + MODULE_SOURCES ../src/_impl.cpp + TEST_SOURCES + main.cpp + test_.cpp +) +``` + +`logos-module-builder` auto-detects `tests/CMakeLists.txt` and creates the `unit-tests` target. + +## Step 9: Run Tests + +```bash +# Unit tests (direct impl class testing, no logoscore needed) +nix build .#unit-tests -L + +# Integration test via logoscore +logoscore -m ./result/lib -l -c ".exampleMethod(test)" +``` + +For mocking other modules or C libraries in tests, see the `testing-modules` skill. + +## Step 10: Inspect ```bash lm ./result/lib/_plugin.so @@ -191,6 +239,8 @@ lm methods ./result/lib/_plugin.so --json - [ ] Impl class name matches `--impl-class` in flake.nix preConfigure - [ ] `CMakeLists.txt` lists generated_code files in SOURCES - [ ] `flake.nix` has preConfigure with logos-cpp-generator +- [ ] `tests/CMakeLists.txt` uses `include(LogosTest)` + `logos_test()` - [ ] All files tracked by git (`git add -A`) - [ ] `nix build` succeeds +- [ ] `nix build .#unit-tests -L` succeeds - [ ] `logoscore` can load and call the module diff --git a/skills/testing-modules/SKILL.md b/skills/testing-modules/SKILL.md index 0198852..74e4927 100644 --- a/skills/testing-modules/SKILL.md +++ b/skills/testing-modules/SKILL.md @@ -1,6 +1,6 @@ --- name: testing-modules -description: Activate when writing tests for Logos modules. Covers unit testing universal modules (direct impl class testing), logoscore integration tests, TEST_GROUPS, mock transport, and Nix check configuration. +description: Activate when writing tests for Logos modules. Covers the logos-test-framework (LOGOS_TEST macros, LogosTestContext, module mocking, C library mocking, event testing), logos_test() CMake integration, logoscore integration tests, and Nix check configuration. --- # Testing Logos Modules @@ -9,90 +9,211 @@ description: Activate when writing tests for Logos modules. Covers unit testing Use this skill when: - Writing unit tests for a universal module +- Setting up test infrastructure (tests/CMakeLists.txt, test files) +- Mocking calls to other modules or external C libraries in tests - Writing integration tests with logoscore -- Adding test infrastructure to a module's flake.nix - Debugging test failures -## Unit Tests (Universal Modules) +## logos-test-framework (Unit Tests) -Universal modules have a plain C++ impl class with no framework dependencies. Test it directly: +The test framework is provided by `logos-module-builder` automatically. No extra flake inputs needed. -### Basic Assert-Based Tests +### File Structure -```cpp -// tests/test_my_module.cpp -#include "../src/my_module_impl.h" -#include -#include - -int main() { - MyModuleImpl impl; - - // Test basic functionality - std::string result = impl.doSomething("test"); - assert(!result.empty()); - - assert(impl.validate("valid_input") == true); - assert(impl.validate("") == false); - - assert(impl.count() >= 0); - - std::cout << "All tests passed" << std::endl; - return 0; -} +``` +tests/ +├── main.cpp # LOGOS_TEST_MAIN() entry point +├── test_my_module.cpp # Test cases using LOGOS_TEST() +└── CMakeLists.txt # logos_test() macro ``` -### SDK Test Framework +### Test Entry Point ```cpp -// tests/test_my_module.cpp -#include "../src/my_module_impl.h" -#include "logos_test.h" - +// tests/main.cpp +#include LOGOS_TEST_MAIN() +``` -TEST(MyModule, DoSomethingWorks) { +### Writing Tests + +```cpp +// tests/test_my_module.cpp +#include +#include "../src/my_module_impl.h" + +LOGOS_TEST(echo_returns_expected_value) { MyModuleImpl impl; - EXPECT_FALSE(impl.doSomething("test").empty()); + LOGOS_ASSERT_EQ(impl.echo("hello"), std::string("echo: hello")); } -TEST(MyModule, ValidateRejectsEmpty) { +LOGOS_TEST(validate_rejects_empty_input) { MyModuleImpl impl; - EXPECT_FALSE(impl.validate("")); + LOGOS_ASSERT_FALSE(impl.validate("")); } ``` -### CMakeLists.txt for Tests +### CMakeLists.txt ```cmake # tests/CMakeLists.txt -add_executable(test_my_module test_my_module.cpp) -target_include_directories(test_my_module PRIVATE ${CMAKE_SOURCE_DIR}/src) -target_link_libraries(test_my_module PRIVATE my_module_impl_objects) +cmake_minimum_required(VERSION 3.14) +project(MyModuleTests LANGUAGES CXX) + +include(LogosTest) + +logos_test( + NAME my_module_tests + MODULE_SOURCES ../src/my_module_impl.cpp + TEST_SOURCES + main.cpp + test_my_module.cpp +) +``` + +### Running Tests + +```bash +nix build .#unit-tests -L # Build and run unit tests +nix flake check -L # Run all Nix checks including tests +``` + +`logos-module-builder` auto-detects `tests/CMakeLists.txt` and creates `checks..unit-tests` and `packages..unit-tests`. + +### Test CLI Options + +```bash +./my_module_tests --filter # Run only matching tests +./my_module_tests --json # JSON output for CI/agents +./my_module_tests --no-color # Disable colored output +./my_module_tests --help # Show help +``` + +## Assertions + +| Macro | Description | +|-------|-------------| +| `LOGOS_ASSERT(expr)` | Expression is truthy | +| `LOGOS_ASSERT_TRUE(expr)` | Alias for LOGOS_ASSERT | +| `LOGOS_ASSERT_FALSE(expr)` | Expression is falsy | +| `LOGOS_ASSERT_EQ(a, b)` | `a == b` with diff on failure | +| `LOGOS_ASSERT_NE(a, b)` | `a != b` | +| `LOGOS_ASSERT_GT(a, b)` | `a > b` | +| `LOGOS_ASSERT_GE(a, b)` | `a >= b` | +| `LOGOS_ASSERT_LT(a, b)` | `a < b` | +| `LOGOS_ASSERT_LE(a, b)` | `a <= b` | +| `LOGOS_ASSERT_CONTAINS(haystack, needle)` | String contains substring | +| `LOGOS_ASSERT_THROWS(expr)` | Expression throws | + +## Mocking Other Modules + +Use `LogosTestContext` to mock calls to other Logos modules: + +```cpp +LOGOS_TEST(calls_other_module) { + auto t = LogosTestContext("my_module"); + t.mockModule("other_module", "getData").returns(42); + + MyModuleImpl impl; + t.init(&impl); + + auto result = impl.fetchData(); + LOGOS_ASSERT_EQ(result, 42); + LOGOS_ASSERT(t.moduleCalled("other_module", "getData")); + LOGOS_ASSERT_EQ(t.moduleCallCount("other_module", "getData"), 1); +} +``` + +## Mocking C Libraries + +For modules wrapping external C/C++ libraries: + +### 1. Write mock stubs + +```cpp +// tests/mocks/mock_libcalc.cpp +#include +extern "C" { #include "libcalc.h" } + +extern "C" int calc_add(int a, int b) { + LOGOS_CMOCK_RECORD("calc_add"); + return LOGOS_CMOCK_RETURN(int, "calc_add"); +} +``` + +### 2. Reference in CMake + +```cmake +logos_test( + NAME calc_module_tests + MODULE_SOURCES ../src/calc_module_impl.cpp + TEST_SOURCES main.cpp test_calc.cpp + MOCK_C_SOURCES mocks/mock_libcalc.cpp +) +``` + +### 3. Use in tests + +```cpp +LOGOS_TEST(add_returns_mocked_value) { + auto t = LogosTestContext("calc_module"); + t.mockCFunction("calc_add").returns(99); + + CalcModuleImpl impl; + t.init(&impl); + + auto result = impl.add(10, 20); + LOGOS_ASSERT_EQ(result, 99); + LOGOS_ASSERT(t.cFunctionCalled("calc_add")); +} +``` + +### 4. Configure Nix for C library mocking + +```nix +logos-module-builder.lib.mkLogosModule { + src = ./.; + configFile = ./metadata.json; + flakeInputs = inputs; + tests = { + dir = ./tests; + mockCLibs = ["mylib"]; + }; +}; +``` + +## Event Testing + +```cpp +LOGOS_TEST(method_emits_event) { + auto t = LogosTestContext("my_module"); + t.captureEvents(); + + MyModuleImpl impl; + t.init(&impl); + + impl.doSomething("data"); + + LOGOS_ASSERT(t.eventEmitted("myEvent")); + LOGOS_ASSERT_EQ(t.eventCount("myEvent"), 1); + LOGOS_ASSERT_EQ(t.lastEventData("myEvent").at(0).toString(), "data"); +} ``` ## Integration Tests with logoscore -Test the module as a loaded plugin: +Test the module as a loaded plugin via the headless runtime: ```bash -# Basic: load and call a method logoscore -m ./result/lib -l my_module \ -c "my_module.doSomething(test_input)" -# Multiple sequential calls logoscore -m ./result/lib -l my_module \ -c "my_module.init(config)" \ -c "my_module.process(data)" -# With multiple modules (tests IPC) logoscore -m ./result/lib -l my_module,other_module \ -c "my_module.callOther(hello)" - -# For CI: exit after calls complete -logoscore -m ./result/lib -l my_module \ - -c "my_module.doSomething(test)" \ - --quit-on-finish ``` ### logoscore Argument Types @@ -104,54 +225,34 @@ Arguments in `-c` calls are auto-detected: - Everything else -> string - `@filename` -> file content as string argument -### Asserting on Output - -In shell-based test scripts, assert on logoscore stdout: - -```bash -OUTPUT=$(logoscore -m ./result/lib -l my_module \ - -c "my_module.doSomething(test)" --quit-on-finish 2>&1) - -if echo "$OUTPUT" | grep -q "expected_result"; then - echo "PASS" -else - echo "FAIL: unexpected output" - exit 1 -fi -``` - -## TEST_GROUPS +### TEST_GROUPS For repos with many tests, group them: ```bash TEST_GROUPS=basic ws test my-module --auto-local TEST_GROUPS=ipc ws test my-module --auto-local -TEST_GROUPS=basic,ipc,errors ws test my-module --auto-local ``` -## Running Tests via Nix +## Running Tests via Nix / Workspace ```bash -# All checks in the flake -nix flake check -L +nix build .#unit-tests -L # Run unit tests +nix flake check -L # All checks in the flake -# In the workspace -ws test my-module -ws test my-module --auto-local - -# All C++ repos -ws test --all --type cpp +ws test my-module # In the workspace +ws test my-module --auto-local # With local dep overrides +ws test --all --type cpp # All C++ repos ``` -## Adding Tests to flake.nix - After adding `checks` outputs to a repo's `flake.nix`, run `ws sync-graph` so the workspace discovers them. ## Key Rules -- Unit tests instantiate the impl class directly — no logoscore, no Qt +- Unit tests use `LOGOS_TEST()` macro, not raw `assert()` or GoogleTest +- Instantiate the impl class directly in unit tests -- no logoscore, no Qt needed +- Use `LogosTestContext` for mocking module calls and C library functions +- `tests/CMakeLists.txt` must use `include(LogosTest)` + `logos_test()` - Integration tests verify the full plugin lifecycle (load, call, response) -- Always use `--quit-on-finish` in CI +- Always use `--quit-on-finish` in CI for logoscore integration tests - 30-second timeout per `-c` call; exit code 1 on failure -- The logoscore binary auto-builds from source on first use diff --git a/templates/universal-module/README.md b/templates/universal-module/README.md index e103d3e..be8e771 100644 --- a/templates/universal-module/README.md +++ b/templates/universal-module/README.md @@ -6,12 +6,24 @@ The scaffold tool in `mcp-server/tools/scaffold.ts` generates files programmatic ## Generated files -- `src/_impl.h` — Pure C++ implementation header (module API) -- `src/_impl.cpp` — Implementation file -- `metadata.json` — Module identity with `"interface": "universal"` -- `CMakeLists.txt` — Uses `logos_module()` macro with generated_code sources -- `flake.nix` — `mkLogosModule` with `preConfigure` running `logos-cpp-generator` -- `tests/test_.cpp` — Unit tests against impl class +- `src/_impl.h` -- Pure C++ implementation header (module API) +- `src/_impl.cpp` -- Implementation file +- `metadata.json` -- Module identity with `"interface": "universal"` +- `CMakeLists.txt` -- Uses `logos_module()` macro with generated_code sources +- `flake.nix` -- `mkLogosModule` with `preConfigure` running `logos-cpp-generator` +- `tests/main.cpp` -- Test runner entry point (`LOGOS_TEST_MAIN()`) +- `tests/test_.cpp` -- Unit tests using `LOGOS_TEST()` macros and assertions +- `tests/CMakeLists.txt` -- `logos_test()` macro integration (auto-detected by `logos-module-builder`) + +## Testing + +The generated tests use logos-test-framework. Run with: + +```bash +nix build .#unit-tests -L +``` + +`logos-module-builder` auto-detects `tests/CMakeLists.txt` and creates `checks..unit-tests` and `packages..unit-tests`. ## Reference diff --git a/tests/run-scaffold-tests.sh b/tests/run-scaffold-tests.sh index 9f267b1..df0cde2 100755 --- a/tests/run-scaffold-tests.sh +++ b/tests/run-scaffold-tests.sh @@ -6,6 +6,7 @@ # 2. `git init`s the scaffolded project (flakes only see tracked files). # 3. Runs `nix build` on the scaffolded project. # 4. Asserts the expected plugin binary exists under ./result/lib/. +# 5. For modules: runs `nix build .#unit-tests -L` to verify generated tests compile and pass. # # Usage: # tests/run-scaffold-tests.sh # one of: module, ui-app @@ -84,6 +85,15 @@ run_one() { fi echo " OK: ${name}_plugin.$ext built" + + if [ "$type" = "module" ]; then + echo " running unit tests..." + ( + cd "$proj" + nix build .#unit-tests -L + ) + echo " OK: unit tests passed" + fi } if [ "$#" -ne 1 ]; then