diff --git a/.gitignore b/.gitignore index ef0967b..2f3228b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /cmake-build-debug/ /.idea /*.zip +/verification/ \ No newline at end of file diff --git a/README.md b/README.md index 008fa7e..417a60f 100644 --- a/README.md +++ b/README.md @@ -356,6 +356,14 @@ ctest --test-dir build --output-on-failure Standalone configuration exposes `STRUCTIVE_BUILD_EXAMPLES`, `STRUCTIVE_BUILD_TESTS` and `STRUCTIVE_INSTALL`. Examples default on only when Structive is the top-level project; tests follow `BUILD_TESTING`; standalone install defaults on. Installation exports `structive::property_core` and `structive::property_extensions` through `find_package(Structive CONFIG)`, and the standalone CTest suite verifies an external install consumer. +Tests are divided by contract so the same behavior is not repeated in one catch-all executable: + +- `property_core_test.cpp`: schema, attributes, explicit validation, typed access, traversal, computed properties and object copy semantics; +- `runtime_api_test.cpp`: type-erased result codes, callback metadata, the copy-write boundary, managed blocking and the read-only zero-lock path; +- `synchronization_test.cpp`: topology, invalid plans, guard held sets, blocking relationships and stable lock order; +- `compile_fail/`: duplicate keys/storage/single-valued attributes, missing keys and invalid capability/constraint/member contracts; +- standalone public-header and install-consumer tests: include self-sufficiency and exported-package boundaries. + ## Documentation - [Design Philosophy and Principles](docs/DESIGN.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 24d5528..0a2be77 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -362,6 +362,14 @@ ctest --test-dir build --output-on-failure 独立构建提供 `STRUCTIVE_BUILD_EXAMPLES`、`STRUCTIVE_BUILD_TESTS`、`STRUCTIVE_INSTALL`。Example 只在 Structive 作为顶层工程时默认开启;测试跟随 `BUILD_TESTING`;独立安装默认开启。安装后可通过 `find_package(Structive CONFIG)` 使用 `structive::property_core` 与 `structive::property_extensions`,并且 standalone CTest 会真实验证外部 install consumer。 +测试按契约分工,避免在一个大用例中重复覆盖: + +- `property_core_test.cpp`:Schema、Attribute、显式 Validation、typed access、traversal、computed property 与对象复制语义; +- `runtime_api_test.cpp`:type-erased result code、callback metadata、copy-write 边界、managed blocking 与只读零锁; +- `synchronization_test.cpp`:topology、非法 Plan、Guard held-set、阻塞关系与稳定锁顺序; +- `compile_fail/`:重复 key/storage/单值 Attribute、缺失 key、capability/constraint/member 不匹配; +- 独立公共头测试与 install consumer:保护 include 自足性和导出包边界。 + ## 详细文档 - [设计理念与原则](docs/DESIGN.zh-CN.md) diff --git a/core/main.cmake b/core/main.cmake index 5fb2eeb..4d8798a 100644 --- a/core/main.cmake +++ b/core/main.cmake @@ -18,6 +18,9 @@ if(STRUCTIVE_BUILD_TESTS) target_link_libraries(structive_property_core_test PRIVATE structive::property_core Threads::Threads) target_link_libraries(structive_property_runtime_api_test PRIVATE structive::property_core Threads::Threads) target_link_libraries(structive_property_synchronization_test PRIVATE structive::property_core Threads::Threads) + target_include_directories(structive_property_core_test PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../tests") + target_include_directories(structive_property_runtime_api_test PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../tests") + target_include_directories(structive_property_synchronization_test PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../tests") if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(structive_property_core_test PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(structive_property_runtime_api_test PRIVATE -Wall -Wextra -Wpedantic) @@ -74,4 +77,5 @@ if(STRUCTIVE_BUILD_TESTS) structive_expect_compile_failure(capability_mismatch "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/capability_mismatch.cpp") structive_expect_compile_failure(incompatible_constraint "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/incompatible_constraint.cpp") structive_expect_compile_failure(foreign_sync_member "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/foreign_sync_member.cpp") + structive_expect_compile_failure(duplicate_attribute "${CMAKE_CURRENT_LIST_DIR}/tests/compile_fail/duplicate_attribute.cpp") endif() diff --git a/core/tests/compile_fail/duplicate_attribute.cpp b/core/tests/compile_fail/duplicate_attribute.cpp new file mode 100644 index 0000000..c73f781 --- /dev/null +++ b/core/tests/compile_fail/duplicate_attribute.cpp @@ -0,0 +1,10 @@ +#include +struct Device { int value{}; }; +int main() { + auto schema = structive::object( + structive::field<&Device::value>( + structive::key<"value">, + structive::unit<"first">, + structive::unit<"second">)); + static_cast(schema); +} diff --git a/core/tests/property_core_test.cpp b/core/tests/property_core_test.cpp index aa34ab8..84c3a91 100644 --- a/core/tests/property_core_test.cpp +++ b/core/tests/property_core_test.cpp @@ -1,13 +1,7 @@ #include -#include -#include -#include -#include -#include +#include "test.hpp" #include -#include #include -#include #include using namespace structive; struct Test_Tag_Category {}; @@ -30,7 +24,6 @@ struct Test_Multi_Attribute { }; template inline constexpr Test_Multi_Attribute test_multi{}; -#define REQUIRE(expression) do { if (!(expression)) { std::fprintf(stderr, "REQUIRE failed: %s:%d: %s\n", __FILE__, __LINE__, #expression); std::abort(); } } while (false) struct Device : Property_Object { Device() = default; explicit Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {} @@ -253,6 +246,9 @@ int main() { auto validation = validate_property_value<&Device::temperature>(device.schema(), 500); REQUIRE(validation.has_value()); REQUIRE(validation->code == "max_value"); + device.write<&Device::temperature>(500); + REQUIRE(device.read<&Device::temperature>() == 500); + device.write<&Device::temperature>(23); REQUIRE(!update_speed_range(device, 200, 100)); REQUIRE(device.read<&Device::min_speed>() == 10); REQUIRE(device.read<&Device::max_speed>() == 100); @@ -288,21 +284,6 @@ int main() { non_copyable_visited = true; }); REQUIRE(non_copyable_visited); - Property_Object_Base& erased = device; - REQUIRE(erased.runtime_object_type() == typeid(Device)); - REQUIRE(erased.runtime_property_count() == 5); - int runtime_value = 0; - REQUIRE(erased.runtime_read("temperature", &runtime_value, &runtime_read_int) == Runtime_Access_Result::ok); - REQUIRE(runtime_value == 23); - REQUIRE(erased.runtime_read("immutable_id", &runtime_value, &runtime_read_int) == Runtime_Access_Result::ok); - REQUIRE(runtime_value == 7); - int runtime_write_value = 35; - REQUIRE(erased.runtime_write("temperature", typeid(int), &runtime_write_value) == Runtime_Access_Result::ok); - REQUIRE(device.temperature == 35); - REQUIRE(erased.runtime_write("immutable_id", typeid(int), &runtime_write_value) == Runtime_Access_Result::not_writable); - double wrong_type = 1.0; - REQUIRE(erased.runtime_write("temperature", typeid(double), &wrong_type) == Runtime_Access_Result::type_mismatch); - REQUIRE(erased.runtime_write("missing", typeid(int), &runtime_write_value) == Runtime_Access_Result::unknown_property); Read_Only_Device read_only_device; REQUIRE(read_only_device.resolved_synchronization().lock_count == 1); REQUIRE(read_only_device.lock_slot<&Read_Only_Device::id>() == Resolved_Synchronization_View::unsynchronized_slot); @@ -314,70 +295,6 @@ int main() { REQUIRE(pure_read_only.lock_slot<&Pure_Read_Only_Device::version>() == Resolved_Synchronization_View::unsynchronized_slot); REQUIRE(pure_read_only.read<&Pure_Read_Only_Device::id>() == 21); REQUIRE(pure_read_only.read<&Pure_Read_Only_Device::version>() == 4); - Property_Object_Base& read_only_erased = read_only_device; - std::binary_semaphore read_only_done{0}; - std::jthread read_only_reader; - { - auto guard = read_only_device.lock_unique<&Read_Only_Device::value>(); - read_only_reader = std::jthread([&] { - int value = 0; - REQUIRE(read_only_erased.runtime_read("id", &value, &runtime_read_int) == Runtime_Access_Result::ok); - REQUIRE(value == 11); - read_only_done.release(); - }); - REQUIRE(read_only_done.try_acquire_for(std::chrono::milliseconds(100))); - } - read_only_reader.join(); - std::binary_semaphore runtime_blocked_done{0}; - std::jthread runtime_writer; - { - auto guard = device.lock_unique<&Device::temperature>(); - runtime_writer = std::jthread([&] { - int value = 36; - REQUIRE(erased.runtime_write("temperature", typeid(int), &value) == Runtime_Access_Result::ok); - runtime_blocked_done.release(); - }); - REQUIRE(!runtime_blocked_done.try_acquire_for(std::chrono::milliseconds(20))); - } - REQUIRE(runtime_blocked_done.try_acquire_for(std::chrono::seconds(2))); - runtime_writer.join(); - REQUIRE(device.read<&Device::temperature>() == 36); - std::binary_semaphore independent_done{0}; - { - auto guard = device.lock_unique({"temperature"}); - std::jthread writer([&] { - device.write<&Device::pressure>(200); - independent_done.release(); - }); - REQUIRE(independent_done.try_acquire_for(std::chrono::seconds(2))); - } - bool dynamic_read_only_unique_rejected = false; - try { - auto guard = device.lock_unique({"immutable_id"}); - (void) guard; - } catch (const std::invalid_argument&) { - dynamic_read_only_unique_rejected = true; - } - REQUIRE(dynamic_read_only_unique_rejected); - std::barrier gate(2); - std::atomic completed{0}; - std::jthread first([&] { - gate.arrive_and_wait(); - auto guard = device.lock_unique({"temperature", "pressure"}); - guard.set<&Device::temperature>(40); - guard.set<&Device::pressure>(140); - completed.fetch_add(1, std::memory_order_release); - }); - std::jthread second([&] { - gate.arrive_and_wait(); - auto guard = device.lock_unique({"pressure", "temperature"}); - guard.set<&Device::pressure>(141); - guard.set<&Device::temperature>(41); - completed.fetch_add(1, std::memory_order_release); - }); - first.join(); - second.join(); - REQUIRE(completed.load(std::memory_order_acquire) == 2); Device copied = device; REQUIRE(copied.temperature == device.temperature); copied.write<&Device::temperature>(99); diff --git a/core/tests/runtime_api_test.cpp b/core/tests/runtime_api_test.cpp index 4c0230f..a2293f0 100644 --- a/core/tests/runtime_api_test.cpp +++ b/core/tests/runtime_api_test.cpp @@ -1,14 +1,12 @@ #include +#include "test.hpp" #include -#include -#include #include #include #include #include #include using namespace structive; -#define REQUIRE(expression) do { if (!(expression)) { std::fprintf(stderr, "REQUIRE failed: %s:%d: %s\n", __FILE__, __LINE__, #expression); std::abort(); } } while (false) struct Runtime_Device : Property_Object { int value{10}; int serial{7}; diff --git a/core/tests/synchronization_test.cpp b/core/tests/synchronization_test.cpp index 64d22d0..2474328 100644 --- a/core/tests/synchronization_test.cpp +++ b/core/tests/synchronization_test.cpp @@ -1,14 +1,12 @@ #include +#include "test.hpp" #include #include -#include -#include #include #include #include #include using namespace structive; -#define REQUIRE(expression) do { if (!(expression)) { std::fprintf(stderr, "REQUIRE failed: %s:%d: %s\n", __FILE__, __LINE__, #expression); std::abort(); } } while (false) struct Sync_Device : Property_Object { Sync_Device() = default; explicit Sync_Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {} @@ -32,6 +30,12 @@ struct structive::Type_Descriptor { static Sync_Device with_plan(Synchronization_Plan plan) { return Sync_Device{property_synchronization(std::move(plan))}; } +template +static void require_invalid_plan(Configure configure) { + Synchronization_Plan plan; + configure(plan); + STRUCTIVE_CHECK_THROWS_AS(with_plan(std::move(plan)), std::invalid_argument); +} static void test_topology_resolution() { Sync_Device independent; auto ro = independent.lock_slot<&Sync_Device::id>(); @@ -67,57 +71,20 @@ static void test_topology_resolution() { REQUIRE(read_only_grouped.resolved_synchronization().lock_count == 3); } static void test_plan_validation() { - bool unknown_thrown = false; - try { - Synchronization_Plan plan; - plan.independent("missing"); - auto device = with_plan(std::move(plan)); - (void)device; - } catch (const std::invalid_argument&) { - unknown_thrown = true; - } - REQUIRE(unknown_thrown); - bool duplicate_property_thrown = false; - try { - Synchronization_Plan plan; - plan.independent("a").unsynchronized("a"); - auto device = with_plan(std::move(plan)); - (void)device; - } catch (const std::invalid_argument&) { - duplicate_property_thrown = true; - } - REQUIRE(duplicate_property_thrown); - bool empty_group_thrown = false; - try { - Synchronization_Plan plan; - plan.group("empty", std::span{}); - auto device = with_plan(std::move(plan)); - (void)device; - } catch (const std::invalid_argument&) { - empty_group_thrown = true; - } - REQUIRE(empty_group_thrown); + require_invalid_plan([](auto& plan) { plan.independent("missing"); }); + require_invalid_plan([](auto& plan) { plan.independent("a").unsynchronized("a"); }); + require_invalid_plan([](auto& plan) { plan.group("empty", {}); }); + require_invalid_plan([](auto& plan) { plan.group("same", {"a"}).group("same", {"b"}); }); } static void test_dynamic_guard_validation() { Sync_Device device; - bool unknown_thrown = false; - try { - auto guard = device.lock_shared({"missing"}); - (void)guard; - } catch (const std::invalid_argument&) { - unknown_thrown = true; - } - REQUIRE(unknown_thrown); - bool read_only_unique_thrown = false; - try { - auto guard = device.lock_unique({"id"}); - (void)guard; - } catch (const std::invalid_argument&) { - read_only_unique_thrown = true; - } - REQUIRE(read_only_unique_thrown); + STRUCTIVE_CHECK_THROWS_AS(device.lock_shared({"missing"}), std::invalid_argument); + STRUCTIVE_CHECK_THROWS_AS(device.lock_unique({"id"}), std::invalid_argument); auto read_guard = device.lock_shared({"id"}); REQUIRE(read_guard.get<"id">() == 1); + STRUCTIVE_CHECK_THROWS_AS(read_guard.get<"a">(), std::logic_error); + auto write_guard = device.lock_unique({"a"}); + STRUCTIVE_CHECK_THROWS_AS(write_guard.set<"b">(21), std::logic_error); } static void test_independent_and_shared_blocking() { Sync_Device independent; diff --git a/extensions/main.cmake b/extensions/main.cmake index 55353ee..8a49772 100644 --- a/extensions/main.cmake +++ b/extensions/main.cmake @@ -14,13 +14,14 @@ endif() if(STRUCTIVE_BUILD_TESTS) add_executable(structive_property_extensions_test "${CMAKE_CURRENT_LIST_DIR}/tests/presentation_test.cpp") target_link_libraries(structive_property_extensions_test PRIVATE structive::property_extensions) + target_include_directories(structive_property_extensions_test PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../tests") if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(structive_property_extensions_test PRIVATE -Wall -Wextra -Wpedantic) endif() add_test(NAME structive_property_extensions_test COMMAND structive_property_extensions_test) set_tests_properties(structive_property_extensions_test PROPERTIES LABELS "unit;extensions") set(header structive/property/extensions/presentation.hpp) - set(target structive_header_structive_property_extensions_presentation_hpp) + set(target structive_header_extensions_presentation_hpp) set(source "${CMAKE_CURRENT_BINARY_DIR}/structive_header_tests/${target}.cpp") file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/structive_header_tests") file(WRITE "${source}" "#include <${header}>\nint main() {\n return 0;\n}\n") diff --git a/extensions/tests/presentation_test.cpp b/extensions/tests/presentation_test.cpp index e997fa3..7fb3aca 100644 --- a/extensions/tests/presentation_test.cpp +++ b/extensions/tests/presentation_test.cpp @@ -1,9 +1,7 @@ #include -#include -#include +#include "test.hpp" #include using namespace structive; -#define REQUIRE(expression) do { if (!(expression)) { std::fprintf(stderr, "REQUIRE failed: %s:%d: %s\n", __FILE__, __LINE__, #expression); std::abort(); } } while (false) struct Device : Property_Object { int temperature{20}; int pressure{100}; diff --git a/tests/test.hpp b/tests/test.hpp new file mode 100644 index 0000000..49af9e6 --- /dev/null +++ b/tests/test.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +namespace structive::test { +inline void check(bool condition, const char* expression, const char* file, int line) { + if (condition) { + return; + } + std::fprintf(stderr, "%s:%d: check failed: %s\n", file, line, expression); + std::abort(); +} + +template +bool throws_as(Function&& function) { + try { + std::forward(function)(); + } catch (const Exception&) { + return true; + } + return false; +} +} + +#define STRUCTIVE_CHECK(expression) ::structive::test::check(static_cast(expression), #expression, __FILE__, __LINE__) +#define STRUCTIVE_CHECK_THROWS_AS(expression, exception) \ + STRUCTIVE_CHECK((::structive::test::throws_as([&] { static_cast(expression); }))) +#define REQUIRE(expression) STRUCTIVE_CHECK(expression)