mirror of
https://github.com/logos-co/logos-test-framework.git
synced 2026-08-27 07:31:07 +00:00
test framework for logos modules
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
# Logos Test Framework
|
||||
|
||||
Unit testing framework for Logos modules. Supports mocking calls to other modules and to libraries.
|
||||
|
||||
## Features
|
||||
|
||||
- **Module mocking** — mock calls to other Logos modules with a fluent API
|
||||
- **C library mocking** — link-time substitution for external C/C++ libraries
|
||||
- **Event testing** — capture and assert on events emitted by the module
|
||||
- **Color terminal output** — auto-detects TTY, clean pass/fail formatting
|
||||
- **JSON output** — `--json` flag for CI and agent consumption
|
||||
- **Test filtering** — `--filter <pattern>` to run a subset of tests
|
||||
- **10-line CMake** — `logos_test()` replaces ~150 lines of boilerplate
|
||||
- **Nix integration** — `mkLogosModuleTests` for zero-config builds
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Write tests
|
||||
|
||||
```cpp
|
||||
// tests/test_my_feature.cpp
|
||||
#include <logos_test.h>
|
||||
#include "my_module_impl.h"
|
||||
|
||||
LOGOS_TEST(feature_returns_expected_value) {
|
||||
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"));
|
||||
}
|
||||
```
|
||||
|
||||
```cpp
|
||||
// tests/main.cpp
|
||||
#include <logos_test.h>
|
||||
LOGOS_TEST_MAIN()
|
||||
```
|
||||
|
||||
### 2. CMake (tests/CMakeLists.txt)
|
||||
|
||||
```cmake
|
||||
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_feature.cpp
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Run
|
||||
|
||||
```bash
|
||||
./my_module_tests # colored output
|
||||
./my_module_tests --json # machine-readable
|
||||
./my_module_tests --filter feature # run matching tests
|
||||
```
|
||||
|
||||
## Mocking Other Modules
|
||||
|
||||
```cpp
|
||||
LOGOS_TEST(calls_waku_module) {
|
||||
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"));
|
||||
LOGOS_ASSERT_EQ(t.moduleCallCount("waku_module", "relayPublish"), 1);
|
||||
}
|
||||
```
|
||||
|
||||
## Mocking C Libraries
|
||||
|
||||
### 1. Write mock stubs (`tests/mocks/mock_libcalc.cpp`)
|
||||
|
||||
```cpp
|
||||
#include <logos_clib_mock.h>
|
||||
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");
|
||||
}
|
||||
|
||||
extern "C" const char* calc_version() {
|
||||
LOGOS_CMOCK_RECORD("calc_version");
|
||||
return LOGOS_CMOCK_RETURN_STRING("calc_version");
|
||||
}
|
||||
```
|
||||
|
||||
### 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"));
|
||||
}
|
||||
```
|
||||
|
||||
## 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");
|
||||
}
|
||||
```
|
||||
|
||||
## 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(h, n)` | String `h` contains `n` |
|
||||
| `LOGOS_ASSERT_THROWS(expr)` | Expression throws |
|
||||
|
||||
## Terminal Output
|
||||
|
||||
```
|
||||
── my_module_tests ─────────────────────────
|
||||
|
||||
PASS feature_returns_expected_value 2ms
|
||||
PASS handles_empty_input 1ms
|
||||
FAIL handles_error_case 3ms
|
||||
ASSERT_EQ failed: expected [true] got [false]
|
||||
at test_my_feature.cpp:42
|
||||
|
||||
── Results: 2 passed, 1 failed (6ms) ──────
|
||||
```
|
||||
|
||||
## Nix Integration
|
||||
|
||||
### In module's flake.nix
|
||||
|
||||
```nix
|
||||
checks.${system}.unit-tests = logos-test-framework.lib.mkLogosModuleTests {
|
||||
inherit pkgs;
|
||||
src = ./.;
|
||||
testDir = ./tests;
|
||||
configFile = ./metadata.json;
|
||||
logosSdk = logos-cpp-sdk.packages.${system}.default;
|
||||
testFramework = logos-test-framework.packages.${system}.default;
|
||||
};
|
||||
```
|
||||
|
||||
### Via logos-module-builder (ultimate goal)
|
||||
|
||||
```nix
|
||||
logos-module-builder.lib.mkLogosModule {
|
||||
src = ./.;
|
||||
configFile = ./metadata.json;
|
||||
flakeInputs = inputs;
|
||||
tests = {
|
||||
dir = ./tests;
|
||||
mockCLibs = ["gowalletsdk"];
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## CLI Options
|
||||
|
||||
```
|
||||
./my_module_tests [options]
|
||||
--filter <pattern> Run only tests whose name contains pattern
|
||||
--json Output results as JSON (for CI/agents)
|
||||
--no-color Disable colored output
|
||||
--help Show help
|
||||
```
|
||||
@@ -0,0 +1,220 @@
|
||||
# LogosTest.cmake
|
||||
# Provides logos_test() — a single CMake function that builds a complete
|
||||
# Logos module test executable with mocking support and minimal boilerplate.
|
||||
#
|
||||
# Usage:
|
||||
# include(LogosTest)
|
||||
#
|
||||
# logos_test(
|
||||
# NAME my_module_tests
|
||||
# MODULE_SOURCES ../src/my_module_impl.cpp
|
||||
# TEST_SOURCES
|
||||
# main.cpp
|
||||
# test_feature_a.cpp
|
||||
# test_feature_b.cpp
|
||||
# MOCK_C_SOURCES # optional: C lib mock stubs
|
||||
# mocks/mock_libcalc.cpp
|
||||
# EXTRA_INCLUDES # optional: additional include dirs
|
||||
# ../lib
|
||||
# GENERATED_SOURCES # optional: generated dispatch code
|
||||
# ../logos_provider_dispatch.cpp
|
||||
# GENERATED_DIR # optional: generated code dir
|
||||
# ../generated_code
|
||||
# )
|
||||
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
#[=======================================================================[.rst:
|
||||
logos_test
|
||||
----------
|
||||
|
||||
Build a Logos module test executable.
|
||||
|
||||
Required:
|
||||
NAME - Test executable name
|
||||
MODULE_SOURCES - Module source files to compile (not the real C lib)
|
||||
TEST_SOURCES - Test source files (main.cpp + test_*.cpp)
|
||||
|
||||
Optional:
|
||||
MOCK_C_SOURCES - Mock implementations for C libraries
|
||||
EXTRA_INCLUDES - Additional include directories
|
||||
GENERATED_SOURCES - Generated code files (logos_provider_dispatch.cpp, etc.)
|
||||
GENERATED_DIR - Directory containing generated code (logos_sdk.cpp, etc.)
|
||||
EXTRA_LINK_LIBS - Additional libraries to link
|
||||
#]=======================================================================]
|
||||
function(logos_test)
|
||||
cmake_parse_arguments(LT ""
|
||||
"NAME;GENERATED_DIR"
|
||||
"MODULE_SOURCES;TEST_SOURCES;MOCK_C_SOURCES;EXTRA_INCLUDES;GENERATED_SOURCES;EXTRA_LINK_LIBS"
|
||||
${ARGN})
|
||||
|
||||
if(NOT LT_NAME)
|
||||
message(FATAL_ERROR "logos_test: NAME is required")
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
# ── Locate logos-test-framework ──────────────────────────────────────────
|
||||
|
||||
if(NOT DEFINED LOGOS_TEST_FRAMEWORK_ROOT)
|
||||
if(DEFINED ENV{LOGOS_TEST_FRAMEWORK_ROOT})
|
||||
set(LOGOS_TEST_FRAMEWORK_ROOT "$ENV{LOGOS_TEST_FRAMEWORK_ROOT}")
|
||||
else()
|
||||
message(FATAL_ERROR "LOGOS_TEST_FRAMEWORK_ROOT not set. "
|
||||
"Set it via environment or CMake variable.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ── Locate logos-cpp-sdk ─────────────────────────────────────────────────
|
||||
|
||||
if(NOT DEFINED LOGOS_CPP_SDK_ROOT)
|
||||
if(DEFINED ENV{LOGOS_CPP_SDK_ROOT})
|
||||
set(LOGOS_CPP_SDK_ROOT "$ENV{LOGOS_CPP_SDK_ROOT}")
|
||||
else()
|
||||
message(FATAL_ERROR "LOGOS_CPP_SDK_ROOT not set. "
|
||||
"Set it via environment or CMake variable.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Detect source vs installed layout for SDK
|
||||
if(EXISTS "${LOGOS_CPP_SDK_ROOT}/cpp/logos_api.h")
|
||||
set(LOGOS_CPP_SDK_IS_SOURCE TRUE)
|
||||
set(SDK_INCLUDE "${LOGOS_CPP_SDK_ROOT}/cpp")
|
||||
set(SDK_MOCK_INCLUDE "${LOGOS_CPP_SDK_ROOT}/cpp/implementations/mock")
|
||||
else()
|
||||
set(LOGOS_CPP_SDK_IS_SOURCE FALSE)
|
||||
set(SDK_INCLUDE "${LOGOS_CPP_SDK_ROOT}/include/cpp")
|
||||
set(SDK_MOCK_INCLUDE "${LOGOS_CPP_SDK_ROOT}/include/cpp/implementations/mock")
|
||||
endif()
|
||||
|
||||
# Detect interface.h location (core/interface.h in SDK)
|
||||
if(EXISTS "${LOGOS_CPP_SDK_ROOT}/core/interface.h")
|
||||
set(SDK_CORE_INCLUDE "${LOGOS_CPP_SDK_ROOT}/core")
|
||||
elseif(EXISTS "${LOGOS_CPP_SDK_ROOT}/include/core/interface.h")
|
||||
set(SDK_CORE_INCLUDE "${LOGOS_CPP_SDK_ROOT}/include/core")
|
||||
else()
|
||||
set(SDK_CORE_INCLUDE "${SDK_INCLUDE}")
|
||||
endif()
|
||||
|
||||
message(STATUS "[LogosTest] SDK root: ${LOGOS_CPP_SDK_ROOT} (source=${LOGOS_CPP_SDK_IS_SOURCE})")
|
||||
message(STATUS "[LogosTest] Framework root: ${LOGOS_TEST_FRAMEWORK_ROOT}")
|
||||
|
||||
# ── Qt ───────────────────────────────────────────────────────────────────
|
||||
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core RemoteObjects)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core RemoteObjects)
|
||||
|
||||
# ── Collect sources ──────────────────────────────────────────────────────
|
||||
|
||||
set(ALL_SOURCES
|
||||
${LT_TEST_SOURCES}
|
||||
${LT_MODULE_SOURCES}
|
||||
${LT_MOCK_C_SOURCES}
|
||||
${LT_GENERATED_SOURCES}
|
||||
# Framework implementation sources
|
||||
${LOGOS_TEST_FRAMEWORK_ROOT}/src/logos_test_runner.cpp
|
||||
${LOGOS_TEST_FRAMEWORK_ROOT}/src/logos_test_context.cpp
|
||||
${LOGOS_TEST_FRAMEWORK_ROOT}/src/logos_clib_mock.cpp
|
||||
)
|
||||
|
||||
# SDK sources (when using source layout — Nix always provides installed layout)
|
||||
if(LOGOS_CPP_SDK_IS_SOURCE)
|
||||
list(APPEND ALL_SOURCES
|
||||
${SDK_INCLUDE}/logos_types.cpp
|
||||
${SDK_INCLUDE}/logos_api.cpp
|
||||
${SDK_INCLUDE}/logos_api_client.cpp
|
||||
${SDK_INCLUDE}/logos_api_consumer.cpp
|
||||
${SDK_INCLUDE}/logos_api_provider.cpp
|
||||
${SDK_INCLUDE}/module_proxy.cpp
|
||||
${SDK_INCLUDE}/token_manager.cpp
|
||||
${SDK_INCLUDE}/logos_transport_factory.cpp
|
||||
${SDK_INCLUDE}/logos_registry_factory.cpp
|
||||
${SDK_INCLUDE}/logos_provider_object.cpp
|
||||
${SDK_INCLUDE}/qt_provider_object.cpp
|
||||
${SDK_INCLUDE}/implementations/qt_local/local_transport.cpp
|
||||
${SDK_INCLUDE}/implementations/qt_remote/remote_transport.cpp
|
||||
${SDK_INCLUDE}/implementations/qt_remote/qt_remote_registry.cpp
|
||||
${SDK_INCLUDE}/implementations/mock/mock_store.cpp
|
||||
${SDK_INCLUDE}/implementations/mock/mock_transport.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
# Look for generated logos_sdk.cpp in GENERATED_DIR
|
||||
if(LT_GENERATED_DIR)
|
||||
if(EXISTS "${LT_GENERATED_DIR}/logos_sdk.cpp")
|
||||
list(APPEND ALL_SOURCES "${LT_GENERATED_DIR}/logos_sdk.cpp")
|
||||
set_source_files_properties("${LT_GENERATED_DIR}/logos_sdk.cpp"
|
||||
PROPERTIES SKIP_AUTOMOC ON)
|
||||
elseif(EXISTS "${LT_GENERATED_DIR}/include/logos_sdk.cpp")
|
||||
list(APPEND ALL_SOURCES "${LT_GENERATED_DIR}/include/logos_sdk.cpp")
|
||||
set_source_files_properties("${LT_GENERATED_DIR}/include/logos_sdk.cpp"
|
||||
PROPERTIES SKIP_AUTOMOC ON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Skip AUTOMOC on generated dispatch files
|
||||
foreach(src ${LT_GENERATED_SOURCES})
|
||||
set_source_files_properties(${src} PROPERTIES SKIP_AUTOMOC ON)
|
||||
endforeach()
|
||||
|
||||
# ── Build test executable ────────────────────────────────────────────────
|
||||
|
||||
add_executable(${LT_NAME} ${ALL_SOURCES})
|
||||
|
||||
target_compile_definitions(${LT_NAME} PRIVATE LOGOS_TESTING=1)
|
||||
|
||||
target_include_directories(${LT_NAME} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../src
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
# Framework headers
|
||||
${LOGOS_TEST_FRAMEWORK_ROOT}/include
|
||||
# SDK headers
|
||||
${SDK_INCLUDE}
|
||||
${SDK_CORE_INCLUDE}
|
||||
${SDK_MOCK_INCLUDE}
|
||||
${SDK_INCLUDE}/implementations/qt_local
|
||||
${SDK_INCLUDE}/implementations/qt_remote
|
||||
)
|
||||
|
||||
# Generated code include
|
||||
if(LT_GENERATED_DIR)
|
||||
target_include_directories(${LT_NAME} PRIVATE
|
||||
${LT_GENERATED_DIR}
|
||||
${LT_GENERATED_DIR}/include
|
||||
)
|
||||
endif()
|
||||
|
||||
# Extra includes
|
||||
foreach(dir ${LT_EXTRA_INCLUDES})
|
||||
target_include_directories(${LT_NAME} PRIVATE ${dir})
|
||||
endforeach()
|
||||
|
||||
# ── Link ─────────────────────────────────────────────────────────────────
|
||||
|
||||
target_link_libraries(${LT_NAME} PRIVATE
|
||||
Qt${QT_VERSION_MAJOR}::Core
|
||||
Qt${QT_VERSION_MAJOR}::RemoteObjects
|
||||
)
|
||||
|
||||
# In installed layout, link against pre-built SDK library
|
||||
if(NOT LOGOS_CPP_SDK_IS_SOURCE)
|
||||
find_library(LOGOS_SDK_LIB logos_sdk
|
||||
PATHS "${LOGOS_CPP_SDK_ROOT}/lib" NO_DEFAULT_PATH REQUIRED)
|
||||
target_link_libraries(${LT_NAME} PRIVATE ${LOGOS_SDK_LIB})
|
||||
endif()
|
||||
|
||||
# Extra link libraries
|
||||
foreach(lib ${LT_EXTRA_LINK_LIBS})
|
||||
target_link_libraries(${LT_NAME} PRIVATE ${lib})
|
||||
endforeach()
|
||||
|
||||
# ── CTest ────────────────────────────────────────────────────────────────
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME ${LT_NAME} COMMAND ${LT_NAME})
|
||||
|
||||
endfunction()
|
||||
@@ -0,0 +1,15 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(BasicModuleTest LANGUAGES CXX)
|
||||
|
||||
# The framework's LogosTest.cmake is found via CMAKE_MODULE_PATH
|
||||
# (set by Nix or by the developer)
|
||||
include(LogosTest)
|
||||
|
||||
logos_test(
|
||||
NAME basic_module_tests
|
||||
MODULE_SOURCES
|
||||
# In a real module: ../src/my_module_impl.cpp
|
||||
TEST_SOURCES
|
||||
main.cpp
|
||||
test_example.cpp
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
#include <logos_test.h>
|
||||
|
||||
LOGOS_TEST_MAIN()
|
||||
@@ -0,0 +1,66 @@
|
||||
// Example: Testing a module that calls another Logos module.
|
||||
// The module under test would normally live in ../src/, but this
|
||||
// is a self-contained example showing the test API.
|
||||
|
||||
#include <logos_test.h>
|
||||
|
||||
// Simulated module method that would call another module
|
||||
static int addViaRemote(LogosTestContext& t) {
|
||||
// In real code this would be impl.callBasicAddInts(10, 20)
|
||||
// which internally calls m_basicClient->invokeRemoteMethod(...)
|
||||
// The mock intercepts at the transport layer.
|
||||
return 30; // placeholder — real tests use actual module impl
|
||||
}
|
||||
|
||||
LOGOS_TEST(mock_module_returns_configured_value) {
|
||||
auto t = LogosTestContext("my_module");
|
||||
t.mockModule("other_module", "addInts").returns(42);
|
||||
// In a real test: MyModuleImpl impl; t.init(&impl);
|
||||
// result = impl.callOtherAddInts(10, 20);
|
||||
// LOGOS_ASSERT_EQ(result, 42);
|
||||
|
||||
// For this example, just verify the API compiles and works
|
||||
LOGOS_ASSERT(true);
|
||||
}
|
||||
|
||||
LOGOS_TEST(mock_module_records_calls) {
|
||||
auto t = LogosTestContext("my_module");
|
||||
t.mockModule("dep_module", "echo").returns("hello back");
|
||||
|
||||
// Verify initial state
|
||||
LOGOS_ASSERT_FALSE(t.moduleCalled("dep_module", "echo"));
|
||||
LOGOS_ASSERT_EQ(t.moduleCallCount("dep_module", "echo"), 0);
|
||||
}
|
||||
|
||||
LOGOS_TEST(c_mock_returns_configured_value) {
|
||||
auto t = LogosTestContext("my_module");
|
||||
t.mockCFunction("calc_add").returns(42);
|
||||
|
||||
int result = LogosCMockStore::instance().getReturn<int>("calc_add");
|
||||
LOGOS_ASSERT_EQ(result, 42);
|
||||
}
|
||||
|
||||
LOGOS_TEST(c_mock_records_calls) {
|
||||
auto t = LogosTestContext("my_module");
|
||||
t.mockCFunction("calc_add").returns(0);
|
||||
|
||||
LogosCMockStore::instance().recordCall("calc_add");
|
||||
|
||||
LOGOS_ASSERT(t.cFunctionCalled("calc_add"));
|
||||
LOGOS_ASSERT_EQ(t.cFunctionCallCount("calc_add"), 1);
|
||||
}
|
||||
|
||||
LOGOS_TEST(assertions_work_correctly) {
|
||||
LOGOS_ASSERT_TRUE(1 == 1);
|
||||
LOGOS_ASSERT_FALSE(1 == 2);
|
||||
LOGOS_ASSERT_EQ(42, 42);
|
||||
LOGOS_ASSERT_NE(1, 2);
|
||||
LOGOS_ASSERT_GT(10, 5);
|
||||
LOGOS_ASSERT_GE(10, 10);
|
||||
LOGOS_ASSERT_LT(5, 10);
|
||||
LOGOS_ASSERT_LE(10, 10);
|
||||
}
|
||||
|
||||
LOGOS_TEST(assert_throws_catches_exception) {
|
||||
LOGOS_ASSERT_THROWS(throw std::runtime_error("expected"));
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(ExtlibModuleTest LANGUAGES CXX)
|
||||
|
||||
include(LogosTest)
|
||||
|
||||
logos_test(
|
||||
NAME extlib_module_tests
|
||||
MODULE_SOURCES
|
||||
# In real module: ../src/calc_module_impl.cpp
|
||||
TEST_SOURCES
|
||||
main.cpp
|
||||
test_extlib.cpp
|
||||
MOCK_C_SOURCES
|
||||
mocks/mock_libcalc.cpp
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
#include <logos_test.h>
|
||||
|
||||
LOGOS_TEST_MAIN()
|
||||
@@ -0,0 +1,15 @@
|
||||
// Mock implementation of libcalc — replaces the real C library at link time.
|
||||
// Each function records its call and returns the value configured via
|
||||
// LogosTestContext::mockCFunction().
|
||||
|
||||
#include <logos_clib_mock.h>
|
||||
|
||||
extern "C" int calc_add(int a, int b) {
|
||||
LOGOS_CMOCK_RECORD("calc_add");
|
||||
return LOGOS_CMOCK_RETURN(int, "calc_add");
|
||||
}
|
||||
|
||||
extern "C" const char* calc_version() {
|
||||
LOGOS_CMOCK_RECORD("calc_version");
|
||||
return LOGOS_CMOCK_RETURN_STRING("calc_version");
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Example: Testing a module that wraps a C library.
|
||||
// The C library is mocked at link time — mock stubs in mocks/ replace
|
||||
// the real library functions.
|
||||
|
||||
#include <logos_test.h>
|
||||
|
||||
// Pretend this is the module's C library header
|
||||
extern "C" {
|
||||
int calc_add(int a, int b);
|
||||
const char* calc_version();
|
||||
}
|
||||
|
||||
LOGOS_TEST(calc_add_returns_mocked_value) {
|
||||
auto t = LogosTestContext("calc_module");
|
||||
t.mockCFunction("calc_add").returns(99);
|
||||
|
||||
// When the module calls calc_add(), it gets the mocked value
|
||||
int result = calc_add(10, 20);
|
||||
LOGOS_ASSERT_EQ(result, 99);
|
||||
LOGOS_ASSERT(t.cFunctionCalled("calc_add"));
|
||||
}
|
||||
|
||||
LOGOS_TEST(calc_version_returns_mocked_string) {
|
||||
auto t = LogosTestContext("calc_module");
|
||||
t.mockCFunction("calc_version").returns("mock-1.0");
|
||||
|
||||
const char* ver = calc_version();
|
||||
LOGOS_ASSERT_EQ(std::string(ver), std::string("mock-1.0"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("calc_version"));
|
||||
}
|
||||
|
||||
LOGOS_TEST(calc_functions_track_call_count) {
|
||||
auto t = LogosTestContext("calc_module");
|
||||
t.mockCFunction("calc_add").returns(0);
|
||||
|
||||
calc_add(1, 2);
|
||||
calc_add(3, 4);
|
||||
calc_add(5, 6);
|
||||
|
||||
LOGOS_ASSERT_EQ(t.cFunctionCallCount("calc_add"), 3);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(IpcModuleTest LANGUAGES CXX)
|
||||
|
||||
include(LogosTest)
|
||||
|
||||
logos_test(
|
||||
NAME ipc_module_tests
|
||||
MODULE_SOURCES
|
||||
# In real module: ../src/test_ipc_new_api_impl.cpp
|
||||
# ../logos_provider_dispatch.cpp
|
||||
TEST_SOURCES
|
||||
main.cpp
|
||||
test_ipc.cpp
|
||||
# GENERATED_SOURCES
|
||||
# ../logos_provider_dispatch.cpp
|
||||
# GENERATED_DIR
|
||||
# ../generated_code
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
#include <logos_test.h>
|
||||
|
||||
LOGOS_TEST_MAIN()
|
||||
@@ -0,0 +1,80 @@
|
||||
// Example: Testing a module that calls other Logos modules via IPC.
|
||||
// Uses LogosTestContext to mock remote module calls.
|
||||
// This mirrors what test-ipc-module-new-api does, but with the new framework.
|
||||
|
||||
#include <logos_test.h>
|
||||
|
||||
// In a real test, you'd include the module's impl header:
|
||||
// #include "test_ipc_new_api_impl.h"
|
||||
|
||||
LOGOS_TEST(mock_basic_addInts) {
|
||||
auto t = LogosTestContext("test_ipc_module");
|
||||
t.mockModule("test_basic_module", "addInts").returns(30);
|
||||
|
||||
// With real module:
|
||||
// TestIpcNewApiImpl impl;
|
||||
// t.init(&impl);
|
||||
// int result = impl.callBasicAddInts(10, 20);
|
||||
// LOGOS_ASSERT_EQ(result, 30);
|
||||
|
||||
// Verify mock API works
|
||||
LOGOS_ASSERT_FALSE(t.moduleCalled("test_basic_module", "addInts"));
|
||||
}
|
||||
|
||||
LOGOS_TEST(mock_extlib_reverse) {
|
||||
auto t = LogosTestContext("test_ipc_module");
|
||||
t.mockModule("test_extlib_module", "reverseString").returns("olleh");
|
||||
|
||||
// With real module:
|
||||
// TestIpcNewApiImpl impl;
|
||||
// t.init(&impl);
|
||||
// QString result = impl.callExtlibReverse("hello");
|
||||
// LOGOS_ASSERT_EQ(result, QString("olleh"));
|
||||
|
||||
LOGOS_ASSERT_EQ(t.moduleCallCount("test_extlib_module", "reverseString"), 0);
|
||||
}
|
||||
|
||||
LOGOS_TEST(mock_chained_calls) {
|
||||
auto t = LogosTestContext("test_ipc_module");
|
||||
t.mockModule("test_basic_module", "echo").returns("hello");
|
||||
t.mockModule("test_extlib_module", "reverseString").returns("olleh");
|
||||
|
||||
// With real module:
|
||||
// TestIpcNewApiImpl impl;
|
||||
// t.init(&impl);
|
||||
// QString result = impl.chainEchoThenReverse("hello");
|
||||
// LOGOS_ASSERT_EQ(result, QString("olleh"));
|
||||
// LOGOS_ASSERT(t.moduleCalled("test_basic_module", "echo"));
|
||||
// LOGOS_ASSERT(t.moduleCalled("test_extlib_module", "reverseString"));
|
||||
|
||||
LOGOS_ASSERT(true); // API compiles correctly
|
||||
}
|
||||
|
||||
LOGOS_TEST(event_capture_api) {
|
||||
auto t = LogosTestContext("test_ipc_module");
|
||||
t.captureEvents();
|
||||
|
||||
// With real module:
|
||||
// TestIpcNewApiImpl impl;
|
||||
// t.init(&impl);
|
||||
// impl.triggerBasicEvent("data");
|
||||
// LOGOS_ASSERT(t.eventEmitted("triggeredBasicEvent"));
|
||||
// LOGOS_ASSERT_EQ(t.eventCount("triggeredBasicEvent"), 1);
|
||||
|
||||
// Verify event API compiles
|
||||
LOGOS_ASSERT_FALSE(t.eventEmitted("nonexistent"));
|
||||
LOGOS_ASSERT_EQ(t.eventCount("nonexistent"), 0);
|
||||
}
|
||||
|
||||
LOGOS_TEST(context_per_test_isolation) {
|
||||
// Each LogosTestContext resets all mocks and state
|
||||
auto t = LogosTestContext("module_a");
|
||||
t.mockCFunction("some_func").returns(100);
|
||||
LogosCMockStore::instance().recordCall("some_func");
|
||||
LOGOS_ASSERT(t.cFunctionCalled("some_func"));
|
||||
|
||||
// Create a new context — state should be fresh
|
||||
auto t2 = LogosTestContext("module_b");
|
||||
LOGOS_ASSERT_FALSE(t2.cFunctionCalled("some_func"));
|
||||
LOGOS_ASSERT_EQ(t2.cFunctionCallCount("some_func"), 0);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
description = "Logos Test Framework — unit testing for Logos modules without Qt boilerplate";
|
||||
|
||||
inputs = {
|
||||
logos-nix.url = "github:logos-co/logos-nix";
|
||||
logos-cpp-sdk.url = "github:logos-co/logos-cpp-sdk";
|
||||
nixpkgs.follows = "logos-nix/nixpkgs";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, logos-nix, logos-cpp-sdk, ... }:
|
||||
let
|
||||
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
|
||||
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f {
|
||||
inherit system;
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
});
|
||||
in
|
||||
{
|
||||
# Library functions for building module tests
|
||||
lib = {
|
||||
mkLogosModuleTests = args: import ./nix/mkLogosModuleTests.nix args;
|
||||
};
|
||||
|
||||
# The framework as a package (headers + cmake + sources)
|
||||
packages = forAllSystems ({ pkgs, system, ... }:
|
||||
let
|
||||
logosSdk = logos-cpp-sdk.packages.${system}.default;
|
||||
|
||||
frameworkPkg = pkgs.stdenv.mkDerivation {
|
||||
pname = "logos-test-framework";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
|
||||
# No build step — just install headers, cmake, and sources
|
||||
dontBuild = true;
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/include $out/cmake $out/src
|
||||
|
||||
cp include/*.h $out/include/
|
||||
cp cmake/*.cmake $out/cmake/
|
||||
cp src/*.cpp $out/src/
|
||||
'';
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "Logos Module Test Framework";
|
||||
license = licenses.mit;
|
||||
};
|
||||
};
|
||||
in {
|
||||
default = frameworkPkg;
|
||||
}
|
||||
);
|
||||
|
||||
# Development shell for working on the framework
|
||||
devShells = forAllSystems ({ pkgs, system, ... }:
|
||||
let
|
||||
logosSdk = logos-cpp-sdk.packages.${system}.default;
|
||||
in {
|
||||
default = pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cmake
|
||||
pkg-config
|
||||
qt6.wrapQtAppsHook
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
qt6.qtbase
|
||||
qt6.qtremoteobjects
|
||||
logosSdk
|
||||
];
|
||||
shellHook = ''
|
||||
export LOGOS_CPP_SDK_ROOT="${logosSdk}"
|
||||
export LOGOS_TEST_FRAMEWORK_ROOT="${./.}"
|
||||
echo "Logos Test Framework development environment"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
#ifndef LOGOS_CLIB_MOCK_H
|
||||
#define LOGOS_CLIB_MOCK_H
|
||||
|
||||
/**
|
||||
* @file logos_clib_mock.h
|
||||
* @brief C library function mock store for link-time substitution.
|
||||
*
|
||||
* When building tests, the real C library is NOT linked. Instead, mock source
|
||||
* files provide the same function signatures backed by LogosCMockStore.
|
||||
*
|
||||
* Example mock file (mocks/mock_libcalc.cpp):
|
||||
*
|
||||
* #include <logos_clib_mock.h>
|
||||
* 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");
|
||||
* }
|
||||
*
|
||||
* In tests:
|
||||
*
|
||||
* LOGOS_TEST(add_works) {
|
||||
* auto t = LogosTestContext("my_module");
|
||||
* t.mockCFunction("calc_add").returns(42);
|
||||
* // ... use module that calls calc_add() ...
|
||||
* }
|
||||
*/
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <cstdint>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LogosCMockStore — singleton registry for C function mocks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class LogosCMockStore {
|
||||
public:
|
||||
static LogosCMockStore& instance();
|
||||
|
||||
void reset();
|
||||
|
||||
// -- Expectation setup ----------------------------------------------------
|
||||
|
||||
class ExpectationBuilder {
|
||||
public:
|
||||
explicit ExpectationBuilder(const std::string& funcName);
|
||||
|
||||
ExpectationBuilder& returns(int value);
|
||||
ExpectationBuilder& returns(bool value);
|
||||
ExpectationBuilder& returns(double value);
|
||||
ExpectationBuilder& returns(const char* value);
|
||||
ExpectationBuilder& returns(const std::string& value);
|
||||
ExpectationBuilder& returnsPtr(const void* ptr);
|
||||
ExpectationBuilder& returnsRaw(const void* data, size_t size);
|
||||
|
||||
private:
|
||||
std::string m_funcName;
|
||||
};
|
||||
|
||||
ExpectationBuilder when(const std::string& funcName);
|
||||
|
||||
// -- Call recording (used by mock implementations) ------------------------
|
||||
|
||||
void recordCall(const std::string& funcName);
|
||||
|
||||
template<typename T>
|
||||
T getReturn(const std::string& funcName) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto it = m_returns.find(funcName);
|
||||
if (it == m_returns.end()) return T{};
|
||||
|
||||
const auto& data = it->second;
|
||||
if (data.size() < sizeof(T)) return T{};
|
||||
|
||||
T val;
|
||||
std::memcpy(&val, data.data(), sizeof(T));
|
||||
return val;
|
||||
}
|
||||
|
||||
// Specialization for const char* — returns a pointer stored as uintptr_t
|
||||
const char* getReturnString(const std::string& funcName) const;
|
||||
|
||||
// -- Verification ---------------------------------------------------------
|
||||
|
||||
bool wasCalled(const std::string& funcName) const;
|
||||
int callCount(const std::string& funcName) const;
|
||||
|
||||
// -- Storage (low-level) --------------------------------------------------
|
||||
|
||||
void setReturn(const std::string& funcName, const void* data, size_t size);
|
||||
void setReturnPtr(const std::string& funcName, const void* ptr);
|
||||
|
||||
private:
|
||||
LogosCMockStore() = default;
|
||||
LogosCMockStore(const LogosCMockStore&) = delete;
|
||||
LogosCMockStore& operator=(const LogosCMockStore&) = delete;
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::unordered_map<std::string, std::vector<uint8_t>> m_returns;
|
||||
std::unordered_map<std::string, int> m_calls;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Convenience macros for mock implementations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#define LOGOS_CMOCK_RECORD(funcName) \
|
||||
LogosCMockStore::instance().recordCall(funcName)
|
||||
|
||||
#define LOGOS_CMOCK_RETURN(type, funcName) \
|
||||
LogosCMockStore::instance().getReturn<type>(funcName)
|
||||
|
||||
#define LOGOS_CMOCK_RETURN_STRING(funcName) \
|
||||
LogosCMockStore::instance().getReturnString(funcName)
|
||||
|
||||
#endif // LOGOS_CLIB_MOCK_H
|
||||
@@ -0,0 +1,241 @@
|
||||
#ifndef LOGOS_TEST_H
|
||||
#define LOGOS_TEST_H
|
||||
|
||||
/**
|
||||
* @file logos_test.h
|
||||
* @brief Logos Module Test Framework — write unit tests without Qt boilerplate.
|
||||
*
|
||||
* Single include for the full test API: runner, assertions, module mocking,
|
||||
* C library mocking, and event testing.
|
||||
*
|
||||
* #include <logos_test.h>
|
||||
*
|
||||
* LOGOS_TEST(my_feature_works) {
|
||||
* auto t = LogosTestContext("my_module");
|
||||
* t.mockModule("dep", "method").returns(42);
|
||||
* MyImpl impl;
|
||||
* t.init(&impl);
|
||||
* LOGOS_ASSERT_EQ(impl.callDep(), 42);
|
||||
* }
|
||||
*/
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
|
||||
// Allow QString and QVariant to be streamed (defined when Qt headers are included)
|
||||
#ifdef QT_CORE_LIB
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
inline std::ostream& operator<<(std::ostream& os, const QString& s) { return os << s.toStdString(); }
|
||||
inline std::ostream& operator<<(std::ostream& os, const QVariant& v) { return os << v.toString().toStdString(); }
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test failure exception
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class LogosTestFailure : public std::runtime_error {
|
||||
public:
|
||||
explicit LogosTestFailure(const std::string& msg) : std::runtime_error(msg) {}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Assertion macros
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#define LOGOS_ASSERT(expr) \
|
||||
do { \
|
||||
if (!(expr)) { \
|
||||
std::ostringstream _oss; \
|
||||
_oss << "ASSERT failed: " #expr \
|
||||
<< " (" << __FILE__ << ":" << __LINE__ << ")"; \
|
||||
throw LogosTestFailure(_oss.str()); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
#define LOGOS_ASSERT_TRUE(expr) LOGOS_ASSERT(expr)
|
||||
#define LOGOS_ASSERT_FALSE(expr) LOGOS_ASSERT(!(expr))
|
||||
|
||||
#define LOGOS_ASSERT_EQ(actual, expected) \
|
||||
do { \
|
||||
auto _logos_a = (actual); \
|
||||
auto _logos_e = (expected); \
|
||||
if (!(_logos_a == _logos_e)) { \
|
||||
std::ostringstream _oss; \
|
||||
_oss << "ASSERT_EQ failed: expected [" \
|
||||
<< _logos_e << "] but got [" << _logos_a << "]" \
|
||||
<< " (" << __FILE__ << ":" << __LINE__ << ")"; \
|
||||
throw LogosTestFailure(_oss.str()); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
#define LOGOS_ASSERT_NE(actual, expected) \
|
||||
do { \
|
||||
auto _logos_a = (actual); \
|
||||
auto _logos_e = (expected); \
|
||||
if (_logos_a == _logos_e) { \
|
||||
std::ostringstream _oss; \
|
||||
_oss << "ASSERT_NE failed: both equal [" << _logos_a << "]" \
|
||||
<< " (" << __FILE__ << ":" << __LINE__ << ")"; \
|
||||
throw LogosTestFailure(_oss.str()); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
#define LOGOS_ASSERT_GT(actual, threshold) \
|
||||
do { \
|
||||
auto _logos_a = (actual); \
|
||||
auto _logos_t = (threshold); \
|
||||
if (!(_logos_a > _logos_t)) { \
|
||||
std::ostringstream _oss; \
|
||||
_oss << "ASSERT_GT failed: " << _logos_a << " is not > " << _logos_t \
|
||||
<< " (" << __FILE__ << ":" << __LINE__ << ")"; \
|
||||
throw LogosTestFailure(_oss.str()); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
#define LOGOS_ASSERT_GE(actual, threshold) \
|
||||
do { \
|
||||
auto _logos_a = (actual); \
|
||||
auto _logos_t = (threshold); \
|
||||
if (!(_logos_a >= _logos_t)) { \
|
||||
std::ostringstream _oss; \
|
||||
_oss << "ASSERT_GE failed: " << _logos_a << " is not >= " << _logos_t \
|
||||
<< " (" << __FILE__ << ":" << __LINE__ << ")"; \
|
||||
throw LogosTestFailure(_oss.str()); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
#define LOGOS_ASSERT_LT(actual, threshold) \
|
||||
do { \
|
||||
auto _logos_a = (actual); \
|
||||
auto _logos_t = (threshold); \
|
||||
if (!(_logos_a < _logos_t)) { \
|
||||
std::ostringstream _oss; \
|
||||
_oss << "ASSERT_LT failed: " << _logos_a << " is not < " << _logos_t \
|
||||
<< " (" << __FILE__ << ":" << __LINE__ << ")"; \
|
||||
throw LogosTestFailure(_oss.str()); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
#define LOGOS_ASSERT_LE(actual, threshold) \
|
||||
do { \
|
||||
auto _logos_a = (actual); \
|
||||
auto _logos_t = (threshold); \
|
||||
if (!(_logos_a <= _logos_t)) { \
|
||||
std::ostringstream _oss; \
|
||||
_oss << "ASSERT_LE failed: " << _logos_a << " is not <= " << _logos_t \
|
||||
<< " (" << __FILE__ << ":" << __LINE__ << ")"; \
|
||||
throw LogosTestFailure(_oss.str()); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
#define LOGOS_ASSERT_CONTAINS(haystack, needle) \
|
||||
do { \
|
||||
auto _logos_h = (haystack); \
|
||||
auto _logos_n = (needle); \
|
||||
if (_logos_h.find(_logos_n) == std::string::npos && \
|
||||
std::string(_logos_h).find(std::string(_logos_n)) == std::string::npos) { \
|
||||
std::ostringstream _oss; \
|
||||
_oss << "ASSERT_CONTAINS failed: [" << _logos_h << "] does not contain [" \
|
||||
<< _logos_n << "]" \
|
||||
<< " (" << __FILE__ << ":" << __LINE__ << ")"; \
|
||||
throw LogosTestFailure(_oss.str()); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
#define LOGOS_ASSERT_THROWS(expr) \
|
||||
do { \
|
||||
bool _logos_threw = false; \
|
||||
try { expr; } catch (...) { _logos_threw = true; } \
|
||||
if (!_logos_threw) { \
|
||||
std::ostringstream _oss; \
|
||||
_oss << "ASSERT_THROWS failed: no exception thrown" \
|
||||
<< " (" << __FILE__ << ":" << __LINE__ << ")"; \
|
||||
throw LogosTestFailure(_oss.str()); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test runner — auto-registers tests via static init
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class LogosTestRunner {
|
||||
public:
|
||||
struct TestEntry {
|
||||
std::string name;
|
||||
std::function<void()> fn;
|
||||
};
|
||||
|
||||
static LogosTestRunner& instance() {
|
||||
static LogosTestRunner runner;
|
||||
return runner;
|
||||
}
|
||||
|
||||
bool registerTest(const char* name, std::function<void()> fn) {
|
||||
m_tests.push_back({name, std::move(fn)});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run all tests. Call from main() — handles arg parsing, QCoreApplication, output.
|
||||
int run(int argc, char* argv[]);
|
||||
|
||||
private:
|
||||
LogosTestRunner() = default;
|
||||
|
||||
struct RunConfig {
|
||||
std::string filter;
|
||||
bool json = false;
|
||||
bool noColor = false;
|
||||
};
|
||||
|
||||
RunConfig parseArgs(int argc, char* argv[]);
|
||||
bool matchesFilter(const std::string& name, const std::string& filter);
|
||||
|
||||
// Output helpers
|
||||
static bool isTTY();
|
||||
static std::string colorGreen(bool enabled) { return enabled ? "\033[32m" : ""; }
|
||||
static std::string colorRed(bool enabled) { return enabled ? "\033[31m" : ""; }
|
||||
static std::string colorYellow(bool enabled) { return enabled ? "\033[33m" : ""; }
|
||||
static std::string colorCyan(bool enabled) { return enabled ? "\033[36m" : ""; }
|
||||
static std::string colorDim(bool enabled) { return enabled ? "\033[2m" : ""; }
|
||||
static std::string colorBold(bool enabled) { return enabled ? "\033[1m" : ""; }
|
||||
static std::string colorReset(bool enabled) { return enabled ? "\033[0m" : ""; }
|
||||
|
||||
std::vector<TestEntry> m_tests;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LOGOS_TEST macro — register a test function at static init time
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#define LOGOS_TEST(name) \
|
||||
static void _logos_test_fn_##name(); \
|
||||
static bool _logos_test_reg_##name = \
|
||||
LogosTestRunner::instance().registerTest(#name, _logos_test_fn_##name); \
|
||||
static void _logos_test_fn_##name()
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LOGOS_TEST_MAIN — generates main() with QCoreApplication + runner
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#define LOGOS_TEST_MAIN() \
|
||||
int main(int argc, char* argv[]) { \
|
||||
return LogosTestRunner::instance().run(argc, argv); \
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Include sub-headers (available after including logos_test.h)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#include "logos_test_context.h"
|
||||
#include "logos_clib_mock.h"
|
||||
|
||||
#endif // LOGOS_TEST_H
|
||||
@@ -0,0 +1,146 @@
|
||||
#ifndef LOGOS_TEST_CONTEXT_H
|
||||
#define LOGOS_TEST_CONTEXT_H
|
||||
|
||||
/**
|
||||
* @file logos_test_context.h
|
||||
* @brief LogosTestContext — hides all Qt/SDK boilerplate from test code.
|
||||
*
|
||||
* Creates LogosAPI in mock mode, manages LogosMockSetup lifecycle, provides
|
||||
* fluent APIs for module mocking, C library mocking, and event capture.
|
||||
*/
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <type_traits>
|
||||
|
||||
// Forward-declare Qt types (only real classes, not typedefs like QVariantList)
|
||||
class QVariant;
|
||||
class LogosAPI;
|
||||
|
||||
#ifdef QT_CORE_LIB
|
||||
#include <QVariantList>
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MockBuilder — fluent builder returned by mockModule()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class MockBuilder {
|
||||
public:
|
||||
MockBuilder(const std::string& module, const std::string& method);
|
||||
|
||||
MockBuilder& returns(int value);
|
||||
MockBuilder& returns(bool value);
|
||||
MockBuilder& returns(double value);
|
||||
MockBuilder& returns(const char* value);
|
||||
MockBuilder& returns(const std::string& value);
|
||||
// For callers that need to pass a QVariant directly (advanced use)
|
||||
MockBuilder& returnsVariant(const QVariant& value);
|
||||
#ifdef QT_CORE_LIB
|
||||
MockBuilder& withArgs(const QVariantList& args);
|
||||
#endif
|
||||
|
||||
private:
|
||||
std::string m_module;
|
||||
std::string m_method;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EventRecord — captured event data
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct EventRecord {
|
||||
std::string name;
|
||||
std::vector<std::string> dataStrings;
|
||||
// Internal: holds the raw QVariantList data (accessible via eventData())
|
||||
void* rawData = nullptr;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LogosTestContext
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class LogosTestContext {
|
||||
public:
|
||||
explicit LogosTestContext(const std::string& moduleName);
|
||||
~LogosTestContext();
|
||||
|
||||
// Non-copyable, non-movable
|
||||
LogosTestContext(const LogosTestContext&) = delete;
|
||||
LogosTestContext& operator=(const LogosTestContext&) = delete;
|
||||
|
||||
// -- Module mocking (wraps LogosMockSetup) --------------------------------
|
||||
|
||||
MockBuilder mockModule(const std::string& module, const std::string& method);
|
||||
bool moduleCalled(const std::string& module, const std::string& method) const;
|
||||
#ifdef QT_CORE_LIB
|
||||
bool moduleCalledWith(const std::string& module, const std::string& method,
|
||||
const QVariantList& args) const;
|
||||
#endif
|
||||
int moduleCallCount(const std::string& module, const std::string& method) const;
|
||||
|
||||
// -- C library mocking (delegates to LogosCMockStore) ---------------------
|
||||
|
||||
class CMockBuilder {
|
||||
public:
|
||||
explicit CMockBuilder(const std::string& funcName);
|
||||
CMockBuilder& returns(int value);
|
||||
CMockBuilder& returns(bool value);
|
||||
CMockBuilder& returns(double value);
|
||||
CMockBuilder& returns(const char* value);
|
||||
CMockBuilder& returns(const std::string& value);
|
||||
CMockBuilder& returnsPtr(const void* ptr);
|
||||
CMockBuilder& returnsRaw(const void* data, size_t size);
|
||||
private:
|
||||
std::string m_funcName;
|
||||
};
|
||||
|
||||
CMockBuilder mockCFunction(const std::string& funcName);
|
||||
bool cFunctionCalled(const std::string& funcName) const;
|
||||
int cFunctionCallCount(const std::string& funcName) const;
|
||||
|
||||
// -- Event capture --------------------------------------------------------
|
||||
|
||||
void captureEvents();
|
||||
bool eventEmitted(const std::string& eventName) const;
|
||||
int eventCount(const std::string& eventName) const;
|
||||
#ifdef QT_CORE_LIB
|
||||
const QVariantList& lastEventData(const std::string& eventName) const;
|
||||
#endif
|
||||
|
||||
// -- Module initialization ------------------------------------------------
|
||||
|
||||
/**
|
||||
* Initialize a new-API module (inherits LogosProviderBase).
|
||||
* Calls impl->init(&logosAPI) internally.
|
||||
*/
|
||||
template<typename T>
|
||||
auto init(T* impl) -> typename std::enable_if<
|
||||
std::is_member_function_pointer<decltype(&T::init)>::value>::type
|
||||
{
|
||||
initProvider(static_cast<void*>(impl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback for legacy QObject-based modules with initLogos(LogosAPI*).
|
||||
* Detected via SFINAE when init() is not available.
|
||||
*/
|
||||
template<typename T>
|
||||
auto initLegacy(T* impl) -> void
|
||||
{
|
||||
initLegacyPlugin(static_cast<void*>(impl));
|
||||
}
|
||||
|
||||
// Access the underlying LogosAPI (advanced use only)
|
||||
LogosAPI* api() const;
|
||||
|
||||
private:
|
||||
void initProvider(void* impl);
|
||||
void initLegacyPlugin(void* impl);
|
||||
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> d;
|
||||
};
|
||||
|
||||
#endif // LOGOS_TEST_CONTEXT_H
|
||||
@@ -0,0 +1,117 @@
|
||||
# mkLogosModuleTests — Nix builder for Logos module unit tests
|
||||
#
|
||||
# Builds the test executable from test sources, module sources, and the
|
||||
# logos-test-framework. Runs the tests as a derivation so they can be
|
||||
# used as `checks.<system>.unit-tests` in a module's flake.
|
||||
#
|
||||
# Usage (in a module's flake.nix):
|
||||
#
|
||||
# checks.${system}.unit-tests = logos-test-framework.lib.mkLogosModuleTests {
|
||||
# inherit pkgs;
|
||||
# src = ./.;
|
||||
# testDir = ./tests;
|
||||
# configFile = ./metadata.json;
|
||||
# logosSdk = logos-cpp-sdk.packages.${system}.default;
|
||||
# testFramework = logos-test-framework.packages.${system}.default;
|
||||
# moduleDeps = { test_basic_module = inputs.test_basic_module.packages.${system}.default; };
|
||||
# mockCLibs = [ "gowalletsdk" ]; # optional
|
||||
# preConfigure = ""; # optional
|
||||
# };
|
||||
|
||||
{ pkgs
|
||||
, src
|
||||
, testDir
|
||||
, configFile ? null
|
||||
, logosSdk
|
||||
, testFramework
|
||||
, moduleDeps ? {}
|
||||
, mockCLibs ? []
|
||||
, preConfigure ? ""
|
||||
, extraBuildInputs ? []
|
||||
, extraCmakeFlags ? []
|
||||
}:
|
||||
|
||||
let
|
||||
lib = pkgs.lib;
|
||||
|
||||
# Copy dependency include files into generated_code/
|
||||
depIncludeSetup = lib.concatMapStringsSep "\n" (name:
|
||||
let dep = moduleDeps.${name} or null;
|
||||
in if dep != null then ''
|
||||
if [ -d "${dep}/include" ]; then
|
||||
echo "Copying include files from ${name}..."
|
||||
cp -r "${dep}/include"/* ./generated_code/ 2>/dev/null || true
|
||||
fi
|
||||
'' else ""
|
||||
) (lib.attrNames moduleDeps);
|
||||
|
||||
in pkgs.stdenv.mkDerivation {
|
||||
pname = "logos-module-tests";
|
||||
version = "0.0.1";
|
||||
|
||||
inherit src;
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cmake
|
||||
pkg-config
|
||||
qt6.wrapQtAppsHook
|
||||
] ++ extraBuildInputs;
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
qt6.qtbase
|
||||
qt6.qtremoteobjects
|
||||
logosSdk
|
||||
testFramework
|
||||
];
|
||||
|
||||
cmakeFlags = [
|
||||
"-DLOGOS_CPP_SDK_ROOT=${logosSdk}"
|
||||
"-DLOGOS_TEST_FRAMEWORK_ROOT=${testFramework}"
|
||||
] ++ extraCmakeFlags;
|
||||
|
||||
# Build from the test directory
|
||||
cmakeDir = toString testDir;
|
||||
|
||||
preConfigure = ''
|
||||
# Set up generated code directory
|
||||
mkdir -p ./generated_code
|
||||
|
||||
# Copy dependency includes
|
||||
${depIncludeSetup}
|
||||
|
||||
# Run logos-cpp-generator if available and metadata exists
|
||||
${lib.optionalString (configFile != null) ''
|
||||
if command -v logos-cpp-generator &>/dev/null && [ -f "${configFile}" ]; then
|
||||
echo "Running logos-cpp-generator..."
|
||||
logos-cpp-generator --metadata "${configFile}" --general-only --output-dir ./generated_code || true
|
||||
fi
|
||||
''}
|
||||
|
||||
# Custom preConfigure
|
||||
${preConfigure}
|
||||
'';
|
||||
|
||||
buildPhase = ''
|
||||
cmake --build . --parallel $NIX_BUILD_CORES
|
||||
'';
|
||||
|
||||
# Run the tests and store the result
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
|
||||
# Find and copy test binary
|
||||
find . -maxdepth 2 -type f -executable -name "*_tests" | head -1 | while read bin; do
|
||||
cp "$bin" $out/bin/
|
||||
done
|
||||
|
||||
# Also try CTest
|
||||
ctest --output-on-failure --timeout 60 || true
|
||||
'';
|
||||
|
||||
# Run tests as a check
|
||||
doCheck = true;
|
||||
checkPhase = ''
|
||||
echo "Running module unit tests..."
|
||||
ctest --output-on-failure --timeout 60
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
#include "logos_clib_mock.h"
|
||||
#include <cstdint>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LogosCMockStore singleton
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
LogosCMockStore& LogosCMockStore::instance() {
|
||||
static LogosCMockStore store;
|
||||
return store;
|
||||
}
|
||||
|
||||
void LogosCMockStore::reset() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_returns.clear();
|
||||
m_calls.clear();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Expectation setup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
LogosCMockStore::ExpectationBuilder LogosCMockStore::when(const std::string& funcName) {
|
||||
return ExpectationBuilder(funcName);
|
||||
}
|
||||
|
||||
LogosCMockStore::ExpectationBuilder::ExpectationBuilder(const std::string& funcName)
|
||||
: m_funcName(funcName) {}
|
||||
|
||||
LogosCMockStore::ExpectationBuilder& LogosCMockStore::ExpectationBuilder::returns(int value) {
|
||||
LogosCMockStore::instance().setReturn(m_funcName, &value, sizeof(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogosCMockStore::ExpectationBuilder& LogosCMockStore::ExpectationBuilder::returns(bool value) {
|
||||
LogosCMockStore::instance().setReturn(m_funcName, &value, sizeof(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogosCMockStore::ExpectationBuilder& LogosCMockStore::ExpectationBuilder::returns(double value) {
|
||||
LogosCMockStore::instance().setReturn(m_funcName, &value, sizeof(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogosCMockStore::ExpectationBuilder& LogosCMockStore::ExpectationBuilder::returns(const char* value) {
|
||||
LogosCMockStore::instance().setReturnPtr(m_funcName, static_cast<const void*>(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogosCMockStore::ExpectationBuilder& LogosCMockStore::ExpectationBuilder::returns(const std::string& value) {
|
||||
LogosCMockStore::instance().setReturnPtr(m_funcName, static_cast<const void*>(value.c_str()));
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogosCMockStore::ExpectationBuilder& LogosCMockStore::ExpectationBuilder::returnsPtr(const void* ptr) {
|
||||
LogosCMockStore::instance().setReturnPtr(m_funcName, ptr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogosCMockStore::ExpectationBuilder& LogosCMockStore::ExpectationBuilder::returnsRaw(const void* data, size_t size) {
|
||||
LogosCMockStore::instance().setReturn(m_funcName, data, size);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Call recording
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void LogosCMockStore::recordCall(const std::string& funcName) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_calls[funcName]++;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Verification
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool LogosCMockStore::wasCalled(const std::string& funcName) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto it = m_calls.find(funcName);
|
||||
return it != m_calls.end() && it->second > 0;
|
||||
}
|
||||
|
||||
int LogosCMockStore::callCount(const std::string& funcName) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto it = m_calls.find(funcName);
|
||||
return (it != m_calls.end()) ? it->second : 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// String return specialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const char* LogosCMockStore::getReturnString(const std::string& funcName) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto it = m_returns.find(funcName);
|
||||
if (it == m_returns.end()) return "";
|
||||
|
||||
const auto& data = it->second;
|
||||
if (data.size() < sizeof(uintptr_t)) return "";
|
||||
|
||||
uintptr_t ptr;
|
||||
std::memcpy(&ptr, data.data(), sizeof(ptr));
|
||||
return reinterpret_cast<const char*>(ptr);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Low-level storage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void LogosCMockStore::setReturn(const std::string& funcName, const void* data, size_t size) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto& vec = m_returns[funcName];
|
||||
vec.resize(size);
|
||||
std::memcpy(vec.data(), data, size);
|
||||
}
|
||||
|
||||
void LogosCMockStore::setReturnPtr(const std::string& funcName, const void* ptr) {
|
||||
uintptr_t addr = reinterpret_cast<uintptr_t>(ptr);
|
||||
setReturn(funcName, &addr, sizeof(addr));
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
#include "logos_test_context.h"
|
||||
#include "logos_clib_mock.h"
|
||||
|
||||
#include <logos_api.h>
|
||||
#include <logos_mode.h>
|
||||
#include <token_manager.h>
|
||||
#include <logos_provider_object.h>
|
||||
#include <implementations/mock/logos_mock.h>
|
||||
#include <implementations/mock/mock_store.h>
|
||||
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <QVariantList>
|
||||
#include <QMetaObject>
|
||||
#include <QDebug>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal event record with full QVariantList
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct InternalEventRecord {
|
||||
std::string name;
|
||||
QVariantList data;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Impl — private implementation hiding all Qt/SDK details
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct LogosTestContext::Impl {
|
||||
std::string moduleName;
|
||||
LogosMockSetup* mockSetup = nullptr;
|
||||
LogosAPI* api = nullptr;
|
||||
bool capturingEvents = false;
|
||||
std::vector<InternalEventRecord> events;
|
||||
static QVariantList s_emptyVariantList;
|
||||
};
|
||||
|
||||
QVariantList LogosTestContext::Impl::s_emptyVariantList;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LogosTestContext
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
LogosTestContext::LogosTestContext(const std::string& moduleName)
|
||||
: d(std::make_unique<Impl>())
|
||||
{
|
||||
d->moduleName = moduleName;
|
||||
|
||||
// Reset C mock store for each test context
|
||||
LogosCMockStore::instance().reset();
|
||||
|
||||
// Activate SDK mock mode (clears MockStore + TokenManager)
|
||||
d->mockSetup = new LogosMockSetup();
|
||||
|
||||
// Create LogosAPI for this module
|
||||
d->api = new LogosAPI(QString::fromStdString(moduleName));
|
||||
}
|
||||
|
||||
LogosTestContext::~LogosTestContext() {
|
||||
delete d->api;
|
||||
delete d->mockSetup;
|
||||
}
|
||||
|
||||
// -- Module mocking -----------------------------------------------------------
|
||||
|
||||
MockBuilder LogosTestContext::mockModule(const std::string& module, const std::string& method) {
|
||||
// Seed a mock token for the target module so LogosAPIClient skips capability_module lookup.
|
||||
// The actual return value expectation is set by MockBuilder::returns().
|
||||
TokenManager::instance().saveToken(QString::fromStdString(module),
|
||||
"mock-token-" + QString::fromStdString(module));
|
||||
return MockBuilder(module, method);
|
||||
}
|
||||
|
||||
bool LogosTestContext::moduleCalled(const std::string& module, const std::string& method) const {
|
||||
return d->mockSetup->wasCalled(QString::fromStdString(module), QString::fromStdString(method));
|
||||
}
|
||||
|
||||
bool LogosTestContext::moduleCalledWith(const std::string& module, const std::string& method,
|
||||
const QVariantList& args) const {
|
||||
return d->mockSetup->wasCalledWith(QString::fromStdString(module),
|
||||
QString::fromStdString(method), args);
|
||||
}
|
||||
|
||||
int LogosTestContext::moduleCallCount(const std::string& module, const std::string& method) const {
|
||||
return d->mockSetup->callCount(QString::fromStdString(module), QString::fromStdString(method));
|
||||
}
|
||||
|
||||
// -- C library mocking --------------------------------------------------------
|
||||
|
||||
LogosTestContext::CMockBuilder LogosTestContext::mockCFunction(const std::string& funcName) {
|
||||
return CMockBuilder(funcName);
|
||||
}
|
||||
|
||||
bool LogosTestContext::cFunctionCalled(const std::string& funcName) const {
|
||||
return LogosCMockStore::instance().wasCalled(funcName);
|
||||
}
|
||||
|
||||
int LogosTestContext::cFunctionCallCount(const std::string& funcName) const {
|
||||
return LogosCMockStore::instance().callCount(funcName);
|
||||
}
|
||||
|
||||
// -- Event capture ------------------------------------------------------------
|
||||
|
||||
void LogosTestContext::captureEvents() {
|
||||
d->capturingEvents = true;
|
||||
}
|
||||
|
||||
bool LogosTestContext::eventEmitted(const std::string& eventName) const {
|
||||
for (const auto& ev : d->events) {
|
||||
if (ev.name == eventName) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int LogosTestContext::eventCount(const std::string& eventName) const {
|
||||
int count = 0;
|
||||
for (const auto& ev : d->events) {
|
||||
if (ev.name == eventName) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const QVariantList& LogosTestContext::lastEventData(const std::string& eventName) const {
|
||||
for (auto it = d->events.rbegin(); it != d->events.rend(); ++it) {
|
||||
if (it->name == eventName) return it->data;
|
||||
}
|
||||
return Impl::s_emptyVariantList;
|
||||
}
|
||||
|
||||
// -- Module initialization ----------------------------------------------------
|
||||
|
||||
void LogosTestContext::initProvider(void* impl) {
|
||||
auto* provider = static_cast<LogosProviderBase*>(impl);
|
||||
|
||||
if (d->capturingEvents) {
|
||||
auto* events = &d->events;
|
||||
provider->setEventListener([events](const QString& name, const QVariantList& data) {
|
||||
events->push_back({name.toStdString(), data});
|
||||
});
|
||||
}
|
||||
|
||||
provider->init(static_cast<void*>(d->api));
|
||||
}
|
||||
|
||||
void LogosTestContext::initLegacyPlugin(void* impl) {
|
||||
auto* obj = static_cast<QObject*>(impl);
|
||||
|
||||
int methodIdx = obj->metaObject()->indexOfMethod("initLogos(LogosAPI*)");
|
||||
if (methodIdx != -1) {
|
||||
QMetaObject::invokeMethod(obj, "initLogos",
|
||||
Qt::DirectConnection,
|
||||
Q_ARG(LogosAPI*, d->api));
|
||||
} else {
|
||||
qWarning() << "LogosTestContext: legacy plugin has no initLogos(LogosAPI*) slot";
|
||||
}
|
||||
}
|
||||
|
||||
LogosAPI* LogosTestContext::api() const {
|
||||
return d->api;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MockBuilder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
MockBuilder::MockBuilder(const std::string& module, const std::string& method)
|
||||
: m_module(module), m_method(method) {}
|
||||
|
||||
MockBuilder& MockBuilder::returns(int value) {
|
||||
MockStore::instance().when(QString::fromStdString(m_module),
|
||||
QString::fromStdString(m_method))
|
||||
.thenReturn(QVariant(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
MockBuilder& MockBuilder::returns(bool value) {
|
||||
MockStore::instance().when(QString::fromStdString(m_module),
|
||||
QString::fromStdString(m_method))
|
||||
.thenReturn(QVariant(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
MockBuilder& MockBuilder::returns(double value) {
|
||||
MockStore::instance().when(QString::fromStdString(m_module),
|
||||
QString::fromStdString(m_method))
|
||||
.thenReturn(QVariant(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
MockBuilder& MockBuilder::returns(const char* value) {
|
||||
MockStore::instance().when(QString::fromStdString(m_module),
|
||||
QString::fromStdString(m_method))
|
||||
.thenReturn(QVariant(QString::fromUtf8(value)));
|
||||
return *this;
|
||||
}
|
||||
|
||||
MockBuilder& MockBuilder::returns(const std::string& value) {
|
||||
MockStore::instance().when(QString::fromStdString(m_module),
|
||||
QString::fromStdString(m_method))
|
||||
.thenReturn(QVariant(QString::fromStdString(value)));
|
||||
return *this;
|
||||
}
|
||||
|
||||
MockBuilder& MockBuilder::returnsVariant(const QVariant& value) {
|
||||
MockStore::instance().when(QString::fromStdString(m_module),
|
||||
QString::fromStdString(m_method))
|
||||
.thenReturn(value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
MockBuilder& MockBuilder::withArgs(const QVariantList& args) {
|
||||
MockStore::instance().when(QString::fromStdString(m_module),
|
||||
QString::fromStdString(m_method))
|
||||
.withArgs(args);
|
||||
return *this;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CMockBuilder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
LogosTestContext::CMockBuilder::CMockBuilder(const std::string& funcName)
|
||||
: m_funcName(funcName) {}
|
||||
|
||||
LogosTestContext::CMockBuilder& LogosTestContext::CMockBuilder::returns(int value) {
|
||||
LogosCMockStore::instance().setReturn(m_funcName, &value, sizeof(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogosTestContext::CMockBuilder& LogosTestContext::CMockBuilder::returns(bool value) {
|
||||
LogosCMockStore::instance().setReturn(m_funcName, &value, sizeof(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogosTestContext::CMockBuilder& LogosTestContext::CMockBuilder::returns(double value) {
|
||||
LogosCMockStore::instance().setReturn(m_funcName, &value, sizeof(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogosTestContext::CMockBuilder& LogosTestContext::CMockBuilder::returns(const char* value) {
|
||||
LogosCMockStore::instance().setReturnPtr(m_funcName, static_cast<const void*>(value));
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogosTestContext::CMockBuilder& LogosTestContext::CMockBuilder::returns(const std::string& value) {
|
||||
// Store a pointer to intern'd string data — caller must keep value alive
|
||||
// For safety, store the pointer via setReturnPtr
|
||||
LogosCMockStore::instance().setReturnPtr(m_funcName, static_cast<const void*>(value.c_str()));
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogosTestContext::CMockBuilder& LogosTestContext::CMockBuilder::returnsPtr(const void* ptr) {
|
||||
LogosCMockStore::instance().setReturnPtr(m_funcName, ptr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
LogosTestContext::CMockBuilder& LogosTestContext::CMockBuilder::returnsRaw(const void* data, size_t size) {
|
||||
LogosCMockStore::instance().setReturn(m_funcName, data, size);
|
||||
return *this;
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
#include "logos_test.h"
|
||||
#include <QCoreApplication>
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#define isatty _isatty
|
||||
#define fileno _fileno
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TTY detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool LogosTestRunner::isTTY() {
|
||||
return isatty(fileno(stdout)) != 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Argument parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
LogosTestRunner::RunConfig LogosTestRunner::parseArgs(int argc, char* argv[]) {
|
||||
RunConfig cfg;
|
||||
cfg.noColor = !isTTY();
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string arg = argv[i];
|
||||
if (arg == "--json") {
|
||||
cfg.json = true;
|
||||
} else if (arg == "--no-color") {
|
||||
cfg.noColor = true;
|
||||
} else if (arg == "--filter" && i + 1 < argc) {
|
||||
cfg.filter = argv[++i];
|
||||
} else if (arg.rfind("--filter=", 0) == 0) {
|
||||
cfg.filter = arg.substr(9);
|
||||
} else if (arg == "--help" || arg == "-h") {
|
||||
std::cout << "Usage: " << (argc > 0 ? argv[0] : "test") << " [options]\n"
|
||||
<< " --filter <pattern> Run only tests matching pattern\n"
|
||||
<< " --json Output results as JSON\n"
|
||||
<< " --no-color Disable colored output\n"
|
||||
<< " --help Show this help\n";
|
||||
std::exit(0);
|
||||
}
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filter matching — simple substring match
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool LogosTestRunner::matchesFilter(const std::string& name, const std::string& filter) {
|
||||
if (filter.empty()) return true;
|
||||
return name.find(filter) != std::string::npos;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON string escaping
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static std::string jsonEscape(const std::string& s) {
|
||||
std::string out;
|
||||
out.reserve(s.size() + 8);
|
||||
for (char c : s) {
|
||||
switch (c) {
|
||||
case '"': out += "\\\""; break;
|
||||
case '\\': out += "\\\\"; break;
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': out += "\\r"; break;
|
||||
case '\t': out += "\\t"; break;
|
||||
default: out += c; break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main run method
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int LogosTestRunner::run(int argc, char* argv[]) {
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
RunConfig cfg = parseArgs(argc, argv);
|
||||
bool color = !cfg.noColor && !cfg.json;
|
||||
|
||||
// Collect tests that match filter
|
||||
std::vector<const TestEntry*> toRun;
|
||||
for (const auto& t : m_tests) {
|
||||
if (matchesFilter(t.name, cfg.filter))
|
||||
toRun.push_back(&t);
|
||||
}
|
||||
|
||||
// Determine suite name from argv[0]
|
||||
std::string suiteName = "logos_tests";
|
||||
if (argc > 0) {
|
||||
std::string exe = argv[0];
|
||||
auto pos = exe.find_last_of("/\\");
|
||||
if (pos != std::string::npos) exe = exe.substr(pos + 1);
|
||||
if (!exe.empty()) suiteName = exe;
|
||||
}
|
||||
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
int skipped = static_cast<int>(m_tests.size()) - static_cast<int>(toRun.size());
|
||||
|
||||
struct TestResult {
|
||||
std::string name;
|
||||
bool pass;
|
||||
double duration_ms;
|
||||
std::string error;
|
||||
};
|
||||
std::vector<TestResult> results;
|
||||
|
||||
auto totalStart = std::chrono::steady_clock::now();
|
||||
|
||||
// Header
|
||||
if (!cfg.json) {
|
||||
std::cout << "\n"
|
||||
<< colorDim(color) << " ── " << colorReset(color)
|
||||
<< colorBold(color) << suiteName << colorReset(color)
|
||||
<< colorDim(color) << " ──────────────────────────────────" << colorReset(color)
|
||||
<< "\n\n";
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for (const auto* t : toRun) {
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
TestResult r;
|
||||
r.name = t->name;
|
||||
r.pass = true;
|
||||
|
||||
if (!cfg.json) {
|
||||
// No prefix here — we'll print result after
|
||||
}
|
||||
|
||||
try {
|
||||
t->fn();
|
||||
} catch (const LogosTestFailure& ex) {
|
||||
r.pass = false;
|
||||
r.error = ex.what();
|
||||
} catch (const std::exception& ex) {
|
||||
r.pass = false;
|
||||
r.error = std::string("Unexpected exception: ") + ex.what();
|
||||
} catch (...) {
|
||||
r.pass = false;
|
||||
r.error = "Unknown exception";
|
||||
}
|
||||
|
||||
auto end = std::chrono::steady_clock::now();
|
||||
r.duration_ms = std::chrono::duration<double, std::milli>(end - start).count();
|
||||
|
||||
if (!cfg.json) {
|
||||
if (r.pass) {
|
||||
std::cout << " " << colorGreen(color) << "PASS" << colorReset(color)
|
||||
<< " " << r.name
|
||||
<< colorDim(color) << " "
|
||||
<< std::fixed << std::setprecision(0)
|
||||
<< r.duration_ms << "ms" << colorReset(color) << "\n";
|
||||
++passed;
|
||||
} else {
|
||||
std::cout << " " << colorRed(color) << "FAIL" << colorReset(color)
|
||||
<< " " << r.name
|
||||
<< colorDim(color) << " "
|
||||
<< std::fixed << std::setprecision(0)
|
||||
<< r.duration_ms << "ms" << colorReset(color) << "\n"
|
||||
<< " " << colorRed(color) << r.error << colorReset(color) << "\n";
|
||||
++failed;
|
||||
}
|
||||
} else {
|
||||
if (r.pass) ++passed; else ++failed;
|
||||
}
|
||||
|
||||
results.push_back(std::move(r));
|
||||
}
|
||||
|
||||
auto totalEnd = std::chrono::steady_clock::now();
|
||||
double totalMs = std::chrono::duration<double, std::milli>(totalEnd - totalStart).count();
|
||||
|
||||
// Footer / summary
|
||||
if (cfg.json) {
|
||||
std::cout << "{\"suite\":\"" << jsonEscape(suiteName) << "\",\"tests\":[";
|
||||
for (size_t i = 0; i < results.size(); ++i) {
|
||||
const auto& r = results[i];
|
||||
if (i > 0) std::cout << ",";
|
||||
std::cout << "{\"name\":\"" << jsonEscape(r.name) << "\","
|
||||
<< "\"status\":\"" << (r.pass ? "pass" : "fail") << "\","
|
||||
<< "\"duration_ms\":" << std::fixed << std::setprecision(1) << r.duration_ms;
|
||||
if (!r.pass) {
|
||||
std::cout << ",\"error\":\"" << jsonEscape(r.error) << "\"";
|
||||
}
|
||||
std::cout << "}";
|
||||
}
|
||||
std::cout << "],\"passed\":" << passed
|
||||
<< ",\"failed\":" << failed
|
||||
<< ",\"skipped\":" << skipped
|
||||
<< ",\"duration_ms\":" << std::fixed << std::setprecision(1) << totalMs
|
||||
<< "}\n";
|
||||
} else {
|
||||
std::cout << "\n"
|
||||
<< colorDim(color) << " ── Results: " << colorReset(color);
|
||||
if (passed > 0)
|
||||
std::cout << colorGreen(color) << passed << " passed" << colorReset(color);
|
||||
if (passed > 0 && failed > 0)
|
||||
std::cout << ", ";
|
||||
if (failed > 0)
|
||||
std::cout << colorRed(color) << failed << " failed" << colorReset(color);
|
||||
if (passed == 0 && failed == 0)
|
||||
std::cout << colorYellow(color) << "no tests matched" << colorReset(color);
|
||||
if (skipped > 0)
|
||||
std::cout << colorDim(color) << ", " << skipped << " skipped" << colorReset(color);
|
||||
std::cout << colorDim(color) << " (" << std::fixed << std::setprecision(0)
|
||||
<< totalMs << "ms)" << colorReset(color);
|
||||
if (toRun.empty()) {
|
||||
std::cout << colorYellow(color) << " (no tests registered!)" << colorReset(color);
|
||||
}
|
||||
std::cout << colorDim(color) << " ──────" << colorReset(color) << "\n\n";
|
||||
}
|
||||
|
||||
return failed > 0 ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user