diff --git a/Adminive-complete-verified-20260808-183859-CST.zip b/Adminive-complete-verified-20260808-183859-CST.zip deleted file mode 100644 index 35807df..0000000 Binary files a/Adminive-complete-verified-20260808-183859-CST.zip and /dev/null differ diff --git a/CMakeLists.txt b/CMakeLists.txt index 66180d8..432d530 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,5 +38,7 @@ add_project_zip_target(package_zip "${CMAKE_CURRENT_LIST_DIR}/Adminive.zip" "${C if(BUILD_TESTING) add_test(NAME Adminive_Install_Consumer_Test COMMAND "${CMAKE_COMMAND}" "-DADMINIVE_BINARY_DIR=${CMAKE_BINARY_DIR}" "-DADMINIVE_CONFIG=$" -P "${CMAKE_CURRENT_LIST_DIR}/cmake/RunInstallConsumer.cmake") set_tests_properties(Adminive_Install_Consumer_Test PROPERTIES LABELS "install;package") + add_test(NAME Adminive_Package_Zip_Test COMMAND "${CMAKE_COMMAND}" "-DADMINIVE_BINARY_DIR=${CMAKE_BINARY_DIR}" "-DADMINIVE_CONFIG=$" -P "${CMAKE_CURRENT_LIST_DIR}/cmake/RunPackageZipTest.cmake") + set_tests_properties(Adminive_Package_Zip_Test PROPERTIES LABELS "package;archive") adminive_register_frontend_tests("${CMAKE_BINARY_DIR}" "$") endif() diff --git a/README.md b/README.md index f8c3145..4d2ad93 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Collection 页面同时是能力实验台。分页、排序、搜索、筛选、 /admin/gallery/items/{id}/status /admin/gallery/managed /admin/gallery/boundaries -/admin/gallery/lab/invalid-filter +/admin/gallery/items?score=92 /admin/gallery/transaction/{success|prepare-reject|commit-fail|rollback-fail} /admin/gallery/backend/view /admin/gallery/backend/amis @@ -825,20 +825,28 @@ ctest --test-dir build --output-on-failure 测试使用 `backend/test_support/adminive_test.hpp` 的 `ADMINIVE_CHECK`,不会像标准 `assert()` 一样在 Release `NDEBUG` 下被移除。旧的 `Synchronized_Value`/`Resource_Lock_Scope` 测试实现已经删除;仍有意义的嵌套同步、无锁字段、读写能力和多线程序列化用例由当前 `Managed_Value` 测试覆盖,不恢复旧 API。 -CTest 使用 label 区分 `unit`、`protocol`、`http`、`install`、`package`、`concurrency`、`managed` 等测试。`Adminive_Install_Consumer_Test` 会先安装到构建目录内的固定测试 prefix,再从独立 `install_consumer` 工程只通过 `find_package(Adminive)` 配置、编译和运行,防止源码 include path 掩盖安装包错误。可选 sanitizer: +CTest 使用 label 区分 `unit`、`protocol`、`http`、`install`、`package`、`header`、`concurrency`、`managed` 等测试。全部 Adminive Core 公共头和 5 个 Service Adapter 公共头都注册独立 include 编译测试,避免 umbrella header 掩盖缺失依赖。`Adminive_Install_Consumer_Test` 会先安装到构建目录内的固定测试 prefix,再从独立 `install_consumer` 工程只通过 `find_package(Adminive)` 同时构建 `Adminive::Core` 与 `Adminive::Default`/Httplib consumer。`Adminive_Package_Zip_Test` 会实际生成源码 ZIP,验证根目录精确文件匹配、无残留嵌套工程且已删除的旧同步实现不会重新进入发布包。 + +Clang/GNU sanitizer: ```powershell cmake -S . -B verification/asan-ubsan -DADMINIVE_ENABLE_ASAN=ON -DADMINIVE_ENABLE_UBSAN=ON cmake -S . -B verification/tsan -DADMINIVE_ENABLE_TSAN=ON ``` -完整 Gate 直接执行: +MSVC 只启用其实际支持的 ASan: + +```powershell +cmake -S . -B verification/asan -DADMINIVE_ENABLE_ASAN=ON +``` + +MSVC 配置 UBSan 或 TSan 会在 CMake 配置阶段明确失败,不允许出现“没有启用 sanitizer 但 Gate 显示通过”的假结果。完整 Gate 直接执行: ```powershell python .\scripts\verify.py ``` -Gate 使用固定 `verification/debug`、`verification/release`、`verification/asan-ubsan` 和 `verification/tsan` 路径,依次完成 Debug/Release 全构建和 CTest、ASan+UBSan Core/Service 矩阵、TSan concurrency、前端依赖/单测/production build、Playwright Chromium E2E;Windows 额外运行 `msedge` channel。ASan+UBSan 矩阵覆盖 Core Adapter、Advanced Adapter、Safety、Managed、View Schema、Drogon 和真实 Httplib,不把耗时的示例 Gallery 重复做 sanitizer 构建。前端 E2E 覆盖导航、同一 Manifest 五种布局、display/create/edit、Collection CRUD/分页/排序/搜索/筛选/状态/重排/错误、422 nested validation、Sensitive 输出过滤、Object Adapter transaction counter、Transaction rollback、状态轮询序列、协议边界和缓存头。需要把前端测试注册进 CTest 时配置 `ADMINIVE_ENABLE_FRONTEND_TESTS=ON`。 +Gate 使用固定 `verification/debug`、`verification/release`、`verification/asan-ubsan` 和 `verification/tsan` 路径。Clang/GNU 运行 Debug/Release 全构建和 CTest、ASan+UBSan Core/Service 矩阵、TSan concurrency、前端依赖/单测/production build、Playwright Chromium E2E;Windows MSVC 只运行真实 ASan,并明确输出 UBSan/TSan SKIP,额外运行 `msedge` channel。Sanitizer 矩阵覆盖 Core Adapter、Advanced Adapter、Safety、Managed、View Schema、Drogon 和真实 Httplib,不把耗时的示例 Gallery 重复做 sanitizer 构建。前端 E2E 覆盖导航、同一 Manifest 五种布局、display/create/edit、Collection CRUD/分页/排序/搜索/筛选/状态/重排/错误、422 nested validation、Sensitive 输出过滤、Object Adapter transaction counter、Transaction rollback、状态轮询序列、协议边界和缓存头。需要把前端测试注册进 CTest 时配置 `ADMINIVE_ENABLE_FRONTEND_TESTS=ON`。 安装并通过 CMake 包使用: diff --git a/backend/library/CMakeLists.txt b/backend/library/CMakeLists.txt index 97aced9..036dd22 100644 --- a/backend/library/CMakeLists.txt +++ b/backend/library/CMakeLists.txt @@ -17,9 +17,15 @@ if (BUILD_TESTING) add_executable(Adminive_Core_Adapter_Test "${CMAKE_CURRENT_LIST_DIR}/tests/core_adapter_test.cpp") target_link_libraries(Adminive_Core_Adapter_Test PRIVATE Adminive::Core) adminive_register_test(Adminive_Core_Adapter_Test "unit;core;adapter") - if (MSVC) + set(adminive_core_public_headers adapter.hpp adminive.hpp amis.hpp collection.hpp composition.hpp concepts.hpp descriptor.hpp http.hpp json.hpp lock.hpp managed.hpp presentation.hpp status.hpp value.hpp view.hpp view_json.hpp) + foreach(header IN LISTS adminive_core_public_headers) + string(REPLACE ".hpp" "" header_name "${header}") + string(REPLACE "_" "-" header_name "${header_name}") + adminive_register_header_test("Adminive_Core_Header_${header_name}_Test" "adminive/${header}" Adminive::Core) + endforeach() + if(MSVC) target_compile_options(Adminive_Core_Adapter_Test PRIVATE /permissive-) - endif () + endif() endif () install(TARGETS Adminive EXPORT AdminiveCoreTargets) install(DIRECTORY "${adminive_library_include_dir}/" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") diff --git a/backend/library/DESIGN.md b/backend/library/DESIGN.md index 4fe957c..5a5df2b 100644 --- a/backend/library/DESIGN.md +++ b/backend/library/DESIGN.md @@ -1007,17 +1007,20 @@ Service 单元测试不能替代 transport 黑盒测试。Httplib adapter test ```text Debug full build + CTest Release full build + CTest -ASan + UBSan -TSan concurrency tests +all public headers standalone include compilation +package ZIP structure verification +Core + Default/Httplib install consumers +ASan + UBSan on Clang/GNU +TSan concurrency tests on Clang/GNU +ASan on MSVC frontend unit tests frontend production build Playwright Chromium E2E Playwright Edge E2E on Windows -install consumer real HTTP integration ``` -CTest label 用于按 `unit/protocol/http/install/package/concurrency/frontend/e2e` 分层定位失败。ASan+UBSan 对 Core Adapter、Advanced Adapter、Safety、Managed、View Schema、Drogon 与真实 Httplib 运行 sanitizer 矩阵,TSan 单独覆盖并发 Managed 路径;Sanitizer 是测试配置,不改变 Core API。frontend E2E 验证同一 Manifest 五种布局、Form modes、Collection CRUD/query/reorder/status、422 nested validation、Sensitive 输出过滤、Object Adapter transaction counter、transaction rollback、状态序列、协议错误和缓存策略。Gate 使用确定的目录和端口,不依赖随机构建路径。 +CTest label 用于按 `unit/protocol/http/install/package/header/concurrency/frontend/e2e` 分层定位失败。Clang/GNU 的 ASan+UBSan 对 Core Adapter、Advanced Adapter、Safety、Managed、View Schema、Drogon 与真实 Httplib 运行 sanitizer 矩阵,TSan 单独覆盖并发 Managed 路径;MSVC 只运行真实 ASan,配置不支持的 UBSan/TSan 必须直接失败而不能假通过。公共头必须逐个独立 include 编译,安装 consumer 同时验证 Core 与 Default/Httplib,源码 ZIP 必须实际生成并检查精确根文件、旧实现残留和嵌套工程。Sanitizer 是测试配置,不改变 Core API。frontend E2E 验证同一 Manifest 五种布局、Form modes、Collection CRUD/query/reorder/status、422 nested validation、Sensitive 输出过滤、Object Adapter transaction counter、transaction rollback、状态序列、协议错误和缓存策略。Gate 使用确定的目录和端口,不依赖随机构建路径。 ## 24. 设计判断清单 diff --git a/backend/service/CMakeLists.txt b/backend/service/CMakeLists.txt index efbe55f..3f46ee0 100644 --- a/backend/service/CMakeLists.txt +++ b/backend/service/CMakeLists.txt @@ -87,6 +87,12 @@ if(BUILD_TESTING) target_include_directories(Adminive_Gallery_Test PRIVATE "${CMAKE_CURRENT_LIST_DIR}/src") target_link_libraries(Adminive_Gallery_Test PRIVATE Adminive_Example) adminive_register_test(Adminive_Gallery_Test "integration;gallery;protocol;http") + adminive_register_header_test(Adminive_Service_Header_Nlohmann_Json_Test "adminive/adapters/nlohmann_json.hpp" Adminive::Nlohmann) + adminive_register_header_test(Adminive_Service_Header_Magic_Enum_Test "adminive/adapters/magic_enum.hpp" Adminive::MagicEnum) + adminive_register_header_test(Adminive_Service_Header_Boost_Pfr_Test "adminive/adapters/boost_pfr.hpp" Adminive::BoostPfr) + adminive_register_header_test(Adminive_Service_Header_Httplib_Test "adminive/adapters/httplib.hpp" Adminive::Httplib) + adminive_register_header_test(Adminive_Service_Header_Drogon_Test "adminive/adapters/drogon.hpp" Adminive::Default) + target_include_directories(Adminive_Service_Header_Drogon_Test BEFORE PRIVATE "${CMAKE_CURRENT_LIST_DIR}/tests/fake_drogon") if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") target_compile_options(Adminive_Test PRIVATE "$<$:-O1>") target_compile_options(Adminive_Config_Store_Test PRIVATE "$<$:-O1>") diff --git a/backend/service/include/adminive/adapters/httplib.hpp b/backend/service/include/adminive/adapters/httplib.hpp index 66b1d0b..cb5812b 100644 --- a/backend/service/include/adminive/adapters/httplib.hpp +++ b/backend/service/include/adminive/adapters/httplib.hpp @@ -2,7 +2,6 @@ #include "adminive/collection.hpp" #include "adminive/http.hpp" #include "httplib.h" -#include #include #include #include @@ -130,11 +129,11 @@ public: query.order_by = request.get_param_value("orderBy"); query.order_dir = request.has_param("orderDir") ? request.get_param_value("orderDir") : "asc"; } - const auto view = describe_table_view(); - for(const auto& column : view.columns) { - if((column.searchable || column.filterable) && request.has_param(column.field)) { - query.fields.insert_or_assign(column.field, request.get_param_value(column.field)); + for(const auto& [name, value] : request.params) { + if(name == "page" || name == "perPage" || name == "orderBy" || name == "orderDir") { + continue; } + query.fields.insert_or_assign(name, value); } write_http_response(response, service->list_response(std::move(query))); }); diff --git a/backend/service/src/example.hpp b/backend/service/src/example.hpp index 687a440..f269c76 100644 --- a/backend/service/src/example.hpp +++ b/backend/service/src/example.hpp @@ -14,6 +14,7 @@ #include namespace adminive::example { using Json = nlohmann::json; +inline constexpr std::string_view radio_states_api = "/admin/radio_states"; template using Http_Resource = adminive::Http_Resource; template diff --git a/backend/service/src/gallery.cpp b/backend/service/src/gallery.cpp index eb024c4..ffef52f 100644 --- a/backend/service/src/gallery.cpp +++ b/backend/service/src/gallery.cpp @@ -362,12 +362,13 @@ Json Component_Gallery::documentation() const { cases.push_back(make_doc_case("polymorphic", "adapters", "Polymorphic Adapter", "variant 的 discriminator、可选类型和每个子类型 Descriptor 都进入协议,AMIS 自动生成类型切换与子表单。", "Polymorphic_Adapter::variants()", examples["polymorphic_descriptor"], examples["polymorphic_amis"], examples["polymorphic_amis"], string_array({"polymorphic_adapter", "discriminator", "variant"}), string_array({"/admin/gallery/advanced/polymorphic/descriptor", "/admin/gallery/advanced/polymorphic/data", "/admin/gallery/advanced/polymorphic/amis"}))); cases.push_back(make_doc_case("object-transaction", "runtime", "Object Adapter + Resource Transaction", "运行时对象通过 Object_Adapter snapshot/commit 映射到可描述 Model;事务实验台覆盖 success、prepare reject、commit fail、rollback fail。", "Object_Adapter; Resource_Transaction", examples["object_adapter_view"], examples["object_adapter_amis"], examples["object_adapter_amis"], string_array({"object_adapter", "resource_transaction", "prepare", "commit", "rollback", "request_context"}), string_array({"/admin/gallery/advanced/object/data", "/admin/gallery/transaction/{scenario}"}))); cases.push_back(make_doc_case("managed-synchronization", "runtime", "Managed_Value 与同步拓扑", "Managed_Value 使用 Structive 的同步拓扑;后端 writable、前端 editable/creatable 和 synchronized 分别展示,避免把权限和锁混为一谈。", "Managed_Value; value.resolved_synchronization(); value.member<&T::field>().write(...)", examples["managed_synchronization"], Json{{"transport", "Resource_Service>"}}, make_gallery_note("Managed / Synchronization", "同步拓扑和字段能力来自运行中的 Managed_Value,而不是第二套前端字段表。"), string_array({"managed_value", "managed_update", "synchronization_topology", "unsynchronized_field", "backend_writable", "frontend_editable"}), string_array({"/admin/gallery/config/data", "/admin/gallery/managed"}))); - cases.push_back(make_doc_case("validation-boundaries", "safety", "Validation 与协议边界", "字段校验、nested path、object validator、readonly 提交、多态 discriminator、非法 Composition/Manifest 和非法 Collection query 都有可重复的标准错误。", "apply_frontend_patch(...); validate_composition_view(...); validate_composition_slot_contracts(...)", examples["protocol_boundaries"], examples["validation_422"], make_gallery_note("协议边界", "在“协议边界”页面可以逐项执行真实请求并查看标准错误。"), string_array({"validation_422", "field_errors", "nested_path", "object_validator", "readonly_reject", "invalid_grid", "invalid_tab", "missing_slot_contract", "duplicate_slot", "invalid_sort", "invalid_filter"}), string_array({"/admin/gallery/boundaries", "/admin/gallery/items?orderBy=color&orderDir=asc", "/admin/gallery/lab/invalid-filter"}))); - cases.push_back(make_doc_case("status", "runtime", "Status Polling:overview + item", "状态 Descriptor 与轮询 renderer 分离;完整运行时同时展示 overview status 和 Collection item status。", "make_amis_status_service(descriptor, api, interval); collection.register_status<&T::status>()", status_descriptor, status_amis, status_amis, string_array({"status_polling", "overview_status", "item_status"}), string_array({"/admin/status", "/admin/radio-states/{id}/status"}))); + cases.push_back(make_doc_case("validation-boundaries", "safety", "Validation 与协议边界", "字段校验、nested path、object validator、readonly 提交、多态 discriminator、非法 Composition/Manifest 和非法 Collection query 都有可重复的标准错误。", "apply_frontend_patch(...); validate_composition_view(...); validate_composition_slot_contracts(...)", examples["protocol_boundaries"], examples["validation_422"], make_gallery_note("协议边界", "在“协议边界”页面可以逐项执行真实请求并查看标准错误。"), string_array({"validation_422", "field_errors", "nested_path", "object_validator", "readonly_reject", "invalid_grid", "invalid_tab", "missing_slot_contract", "duplicate_slot", "invalid_sort", "invalid_filter"}), string_array({"/admin/gallery/boundaries", "/admin/gallery/items?orderBy=color&orderDir=asc", "/admin/gallery/items?score=92"}))); + Json status_endpoints = Json::array({"/admin/status", std::string(radio_states_api) + "/{id}/status"}); + cases.push_back(make_doc_case("status", "runtime", "Status Polling:overview + item", "状态 Descriptor 与轮询 renderer 分离;完整运行时同时展示 overview status 和 Collection item status。", "make_amis_status_service(descriptor, api, interval); collection.register_status<&T::status>()", status_descriptor, status_amis, status_amis, string_array({"status_polling", "overview_status", "item_status"}), std::move(status_endpoints))); cases.push_back(make_doc_case("http-adapters", "adapters", "HTTP Adapter:Httplib + Drogon", "Httplib 现在与 Drogon 一样可以通过 context_factory 构造 Request_Context;真实 localhost 黑盒测试覆盖 CRUD、查询、状态和上下文传递。", "resource.bind(server, Httplib_Bind_Options{.context_factory = ...}); Drogon_Bind_Options{...}", Json{{"httplib", "context_factory -> Request_Context"}, {"drogon", "context_factory + executor/filter"}}, Json{{"tests", string_array({"Adminive_Httplib_Adapter_Test", "Adminive_Drogon_Adapter_Test"})}}, make_gallery_note("Transport Adapter", "当前 Gallery 本身运行在 Httplib;Drogon 契约由同一 service 层和独立 adapter test 验证。"), string_array({"httplib", "drogon", "request_context", "real_http_test"}), string_array({"/admin/gallery/config/data"}))); - const Json verification_view = Json{{"ctest_labels", string_array({"unit", "protocol", "http", "install", "concurrency", "package"})}, {"sanitizers", string_array({"ASan", "UBSan", "TSan"})}, {"configurations", string_array({"Debug", "Release"})}}; - const Json verification_runtime = Json{{"frontend", string_array({"node_test", "playwright_chromium", "playwright_msedge"})}}; - cases.push_back(make_doc_case("verification", "engineering", "工程验证 Gate", "Release-safe test checks、安装消费测试、真实 HTTP、CTest labels、Debug/Release、ASan/UBSan/TSan、前端单测和 Playwright 都有正式入口。", "ctest -L unit; ctest -L http; ctest -L install; python scripts/verify.py", verification_view, verification_runtime, make_gallery_note("Verification Gate", "测试断言在 Release 下不会被 NDEBUG 清除;install consumer 只通过 find_package(Adminive) 消费安装结果。"), string_array({"release_safe_checks", "install_consumer", "debug", "release", "asan", "ubsan", "tsan", "frontend_unit", "playwright", "edge"}))); + const Json verification_view = Json{{"ctest_labels", string_array({"unit", "protocol", "http", "install", "package", "header", "concurrency"})}, {"sanitizer_matrix", Json{{"clang_gnu", string_array({"ASan", "UBSan", "TSan"})}, {"msvc", string_array({"ASan"})}, {"unsupported_configuration", "configure_error"}}}, {"configurations", string_array({"Debug", "Release"})}}; + const Json verification_runtime = Json{{"headers", "all public Adminive headers compile standalone"}, {"install_consumers", string_array({"Adminive::Core", "Adminive::Default + Httplib"})}, {"package_zip", "exact root files + no stale nested project"}, {"frontend", string_array({"node_test", "playwright_chromium", "playwright_msedge"})}}; + cases.push_back(make_doc_case("verification", "engineering", "工程验证 Gate", "Release-safe test checks、公共头独立编译、源码包结构、Core + Default/Httplib 安装消费、真实 HTTP、Debug/Release 和平台真实支持的 Sanitizer 都有正式入口。", "ctest -L header; ctest -L package; ctest -L install; python scripts/verify.py", verification_view, verification_runtime, make_gallery_note("Verification Gate", "测试断言在 Release 下不会被 NDEBUG 清除;不支持的 sanitizer 配置会明确失败或 SKIP,不会假通过。"), string_array({"release_safe_checks", "public_header_compile", "package_zip", "install_consumer", "debug", "release", "asan", "ubsan", "tsan", "frontend_unit", "playwright", "edge"}))); Json layers = Json::array(); layers.push_back(Json{{"name", "Object Schema"}, {"purpose", "字段结构、读写能力、约束与同步事实"}}); layers.push_back(Json{{"name", "Field Presentation"}, {"purpose", "单字段 label、description、control、options、visible_on"}}); @@ -416,12 +417,6 @@ void Component_Gallery::bind(httplib::Server& server) { server.Get("/admin/gallery/boundaries", [](const httplib::Request&, httplib::Response& response) { write_http_response(response, make_http_success(protocol_boundaries())); }); - server.Get("/admin/gallery/lab/invalid-filter", [](const httplib::Request&, httplib::Response& response) { - Collection_Service collection("/lab", make_rows()); - Collection_Query query; - query.fields.emplace("score", "92"); - write_http_response(response, collection.list_response(std::move(query))); - }); server.Post(R"(/admin/gallery/transaction/(success|prepare-reject|commit-fail|rollback-fail))", [](const httplib::Request& request, httplib::Response& response) { write_http_response(response, transaction_scenario(request.matches[1].str(), request)); }); diff --git a/backend/service/src/server_app.cpp b/backend/service/src/server_app.cpp index 048bf93..9968172 100644 --- a/backend/service/src/server_app.cpp +++ b/backend/service/src/server_app.cpp @@ -12,7 +12,7 @@ Radio_State make_radio_state(Radio_Mode mode, int port, int buffer_count, int lo return result; } } -Server_App::Server_App(int port, std::filesystem::path frontend_dist, std::filesystem::path config_file) : port_(port), frontend_dist_(std::move(frontend_dist)), config_store_(std::move(config_file)), state_resource_("/admin/radio_states", make_radio_states()), service_config_resource_(config_store_.radio_service(), "/admin/config/radio", config_store_.radio_service_transaction()), alert_config_resource_(config_store_.alerts(), "/admin/config/alerts", config_store_.alerts_transaction()), device_tables_(config_store_) { +Server_App::Server_App(int port, std::filesystem::path frontend_dist, std::filesystem::path config_file) : port_(port), frontend_dist_(std::move(frontend_dist)), config_store_(std::move(config_file)), state_resource_(std::string(radio_states_api), make_radio_states()), service_config_resource_(config_store_.radio_service(), "/admin/config/radio", config_store_.radio_service_transaction()), alert_config_resource_(config_store_.alerts(), "/admin/config/alerts", config_store_.alerts_transaction()), device_tables_(config_store_) { server_.set_file_request_handler([](const httplib::Request& request, httplib::Response& response) { if(request.path == "/" || request.path == "/index.html") { response.set_header("Cache-Control", "no-store, no-cache, must-revalidate"); diff --git a/backend/service/tests/gallery_test.cpp b/backend/service/tests/gallery_test.cpp index fcddac0..dff70b8 100644 --- a/backend/service/tests/gallery_test.cpp +++ b/backend/service/tests/gallery_test.cpp @@ -25,6 +25,15 @@ int main() { httplib::Client client("127.0.0.1", port); const Json docs = body_json(client.Get("/admin/gallery/docs")).at("data"); ADMINIVE_CHECK(docs.at("cases").size() == 14); + bool status_endpoint_verified{}; + for(const auto& item : docs.at("cases")) { + if(item.at("id") != "status") { + continue; + } + ADMINIVE_CHECK(item.at("endpoints") == Json::array({"/admin/status", "/admin/radio_states/{id}/status"})); + status_endpoint_verified = true; + } + ADMINIVE_CHECK(status_endpoint_verified); ADMINIVE_CHECK(docs.at("capabilities").at("adapters").size() > 5); ADMINIVE_CHECK(docs.at("examples").at("form_create_view").at("mode") == "create"); std::size_t creatable_fields = 0; @@ -63,7 +72,7 @@ int main() { ADMINIVE_CHECK(boundaries.size() == 11); ADMINIVE_CHECK(boundaries.at(4).at("id") == "manifest-duplicate-slot"); ADMINIVE_CHECK(boundaries.at(4).at("rejected") == true); - check_status(client.Get("/admin/gallery/lab/invalid-filter"), 400); + check_status(client.Get("/admin/gallery/items?score=92"), 400); const Json item_status = body_json(client.Get("/admin/gallery/items/1/status")).at("data"); ADMINIVE_CHECK(item_status.at("state").at("value") == "enabled"); httplib::Headers headers{{"X-Adminive-User", "gallery-test"}, {"X-Request-Id", "request-1"}}; diff --git a/backend/service/tests/httplib_adapter_test.cpp b/backend/service/tests/httplib_adapter_test.cpp index 297028c..46bdd83 100644 --- a/backend/service/tests/httplib_adapter_test.cpp +++ b/backend/service/tests/httplib_adapter_test.cpp @@ -112,6 +112,8 @@ int main() { ADMINIVE_CHECK(body_json(client.Get("/rows?perPage=2&page=1&orderBy=score&orderDir=desc")).at("data").at("items").at(0).at("name") == "beta"); ADMINIVE_CHECK(body_json(client.Get("/rows?name=alp")).at("data").at("total") == 1); ADMINIVE_CHECK(body_json(client.Get("/rows?enabled=true")).at("data").at("total") == 2); + check_status(client.Get("/rows?score=90"), 400); + check_status(client.Get("/rows?unknown=value"), 400); check_status(client.Get("/rows?orderBy=enabled&orderDir=asc"), 400); check_status(client.Get("/rows?orderBy=score&orderDir=sideways"), 400); const auto created = body_json(client.Post("/rows", R"({"name":"delta","enabled":true,"score":80})", "application/json")); diff --git a/cmake/AdminiveVerification.cmake b/cmake/AdminiveVerification.cmake index b31078d..cb5aa04 100644 --- a/cmake/AdminiveVerification.cmake +++ b/cmake/AdminiveVerification.cmake @@ -5,6 +5,12 @@ option(ADMINIVE_ENABLE_FRONTEND_TESTS "Register Adminive frontend unit and brows if(ADMINIVE_ENABLE_ASAN AND ADMINIVE_ENABLE_TSAN) message(FATAL_ERROR "ASan and TSan cannot be enabled together") endif() +if((ADMINIVE_ENABLE_ASAN OR ADMINIVE_ENABLE_UBSAN OR ADMINIVE_ENABLE_TSAN) AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU|MSVC") + message(FATAL_ERROR "Requested sanitizer configuration is unsupported by ${CMAKE_CXX_COMPILER_ID}") +endif() +if(MSVC AND (ADMINIVE_ENABLE_UBSAN OR ADMINIVE_ENABLE_TSAN)) + message(FATAL_ERROR "MSVC verification supports ASan only; UBSan and TSan require Clang or GNU") +endif() get_filename_component(adminive_verification_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) set(adminive_test_support_dir "${adminive_verification_root}/backend/test_support") function(adminive_configure_sanitizers target) @@ -21,6 +27,9 @@ function(adminive_configure_sanitizers target) target_compile_options(${target} PRIVATE -fsanitize=thread -fno-omit-frame-pointer) target_link_options(${target} PRIVATE -fsanitize=thread) endif() + elseif(MSVC AND ADMINIVE_ENABLE_ASAN) + target_compile_options(${target} PRIVATE /fsanitize=address) + target_link_options(${target} PRIVATE /fsanitize=address) endif() endfunction() function(adminive_configure_test_target target) @@ -35,6 +44,16 @@ function(adminive_register_test target labels) set_tests_properties(${target} PROPERTIES LABELS "${labels}") adminive_configure_test_target(${target}) endfunction() +function(adminive_register_header_test target header link_target) + set(source "${CMAKE_CURRENT_BINARY_DIR}/${target}.cpp") + file(GENERATE OUTPUT "${source}" CONTENT "#include <${header}>\nint main() {\n return 0;\n}\n") + add_executable(${target} "${source}") + target_link_libraries(${target} PRIVATE ${link_target}) + if(MSVC) + target_compile_options(${target} PRIVATE /permissive-) + endif() + adminive_register_test(${target} "unit;header;compile") +endfunction() function(adminive_register_frontend_tests binary_dir configuration) if(NOT ADMINIVE_ENABLE_FRONTEND_TESTS) return() diff --git a/cmake/PackageZip.cmake b/cmake/PackageZip.cmake index 36fdc92..3fec0d0 100644 --- a/cmake/PackageZip.cmake +++ b/cmake/PackageZip.cmake @@ -1,35 +1,38 @@ +function(package_collect_matches output root pattern) + string(REPLACE "\\" "/" pattern "${pattern}") + string(REGEX REPLACE "^/+" "" pattern "${pattern}") + if(pattern STREQUAL "") + set(${output} PARENT_SCOPE) + return() + endif() + if(pattern MATCHES "/$") + string(REGEX REPLACE "/+$" "" pattern "${pattern}") + file(GLOB_RECURSE matched_files LIST_DIRECTORIES false RELATIVE "${root}" "${root}/${pattern}/*") + else() + string(FIND "${pattern}" "*" wildcard_star) + string(FIND "${pattern}" "?" wildcard_question) + if(NOT wildcard_star EQUAL -1 OR NOT wildcard_question EQUAL -1) + file(GLOB_RECURSE matched_files LIST_DIRECTORIES false RELATIVE "${root}" "${root}/${pattern}") + elseif(EXISTS "${root}/${pattern}" AND NOT IS_DIRECTORY "${root}/${pattern}") + set(matched_files "${pattern}") + else() + set(matched_files) + endif() + endif() + set(${output} ${matched_files} PARENT_SCOPE) +endfunction() if(PACKAGE_ZIP_RUN) file(STRINGS "${PACKAGE_INCLUDE_FILE}" package_includes) file(STRINGS "${PACKAGE_EXCLUDE_FILE}" package_excludes) set(package_files) foreach(include_pattern IN LISTS package_includes) - string(REPLACE "\\" "/" include_pattern "${include_pattern}") - string(REGEX REPLACE "^/+" "" include_pattern "${include_pattern}") - if(include_pattern STREQUAL "") - continue() - endif() - if(include_pattern MATCHES "/$") - string(REGEX REPLACE "/+$" "" include_pattern "${include_pattern}") - file(GLOB_RECURSE matched_files LIST_DIRECTORIES false RELATIVE "${PACKAGE_ROOT}" "${PACKAGE_ROOT}/${include_pattern}/*") - else() - file(GLOB_RECURSE matched_files LIST_DIRECTORIES false RELATIVE "${PACKAGE_ROOT}" "${PACKAGE_ROOT}/${include_pattern}") - endif() + package_collect_matches(matched_files "${PACKAGE_ROOT}" "${include_pattern}") list(APPEND package_files ${matched_files}) endforeach() list(REMOVE_DUPLICATES package_files) set(excluded_files) foreach(exclude_pattern IN LISTS package_excludes) - string(REPLACE "\\" "/" exclude_pattern "${exclude_pattern}") - string(REGEX REPLACE "^/+" "" exclude_pattern "${exclude_pattern}") - if(exclude_pattern STREQUAL "") - continue() - endif() - if(exclude_pattern MATCHES "/$") - string(REGEX REPLACE "/+$" "" exclude_pattern "${exclude_pattern}") - file(GLOB_RECURSE matched_files LIST_DIRECTORIES false RELATIVE "${PACKAGE_ROOT}" "${PACKAGE_ROOT}/${exclude_pattern}/*") - else() - file(GLOB_RECURSE matched_files LIST_DIRECTORIES false RELATIVE "${PACKAGE_ROOT}" "${PACKAGE_ROOT}/${exclude_pattern}") - endif() + package_collect_matches(matched_files "${PACKAGE_ROOT}" "${exclude_pattern}") list(APPEND excluded_files ${matched_files}) endforeach() if(excluded_files) diff --git a/cmake/RunInstallConsumer.cmake b/cmake/RunInstallConsumer.cmake index c32358d..f764b0b 100644 --- a/cmake/RunInstallConsumer.cmake +++ b/cmake/RunInstallConsumer.cmake @@ -24,10 +24,16 @@ if(NOT build_result EQUAL 0) endif() if(WIN32) set(adminive_consumer "${adminive_consumer_binary}/${ADMINIVE_CONFIG}/consumer.exe") + set(adminive_default_consumer "${adminive_consumer_binary}/${ADMINIVE_CONFIG}/default_consumer.exe") else() set(adminive_consumer "${adminive_consumer_binary}/consumer") + set(adminive_default_consumer "${adminive_consumer_binary}/default_consumer") endif() execute_process(COMMAND "${adminive_consumer}" RESULT_VARIABLE run_result) if(NOT run_result EQUAL 0) - message(FATAL_ERROR "Install consumer run failed: ${run_result}") + message(FATAL_ERROR "Core install consumer run failed: ${run_result}") +endif() +execute_process(COMMAND "${adminive_default_consumer}" RESULT_VARIABLE default_run_result) +if(NOT default_run_result EQUAL 0) + message(FATAL_ERROR "Default/Httplib install consumer run failed: ${default_run_result}") endif() diff --git a/cmake/RunPackageZipTest.cmake b/cmake/RunPackageZipTest.cmake new file mode 100644 index 0000000..451fc71 --- /dev/null +++ b/cmake/RunPackageZipTest.cmake @@ -0,0 +1,38 @@ +if(NOT DEFINED ADMINIVE_BINARY_DIR) + message(FATAL_ERROR "ADMINIVE_BINARY_DIR is required") +endif() +if(NOT DEFINED ADMINIVE_CONFIG OR ADMINIVE_CONFIG STREQUAL "") + set(ADMINIVE_CONFIG Release) +endif() +get_filename_component(adminive_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) +set(adminive_zip "${adminive_root}/Adminive.zip") +execute_process(COMMAND "${CMAKE_COMMAND}" --build "${ADMINIVE_BINARY_DIR}" --target package_zip --config "${ADMINIVE_CONFIG}" RESULT_VARIABLE build_result) +if(NOT build_result EQUAL 0) + message(FATAL_ERROR "package_zip build failed: ${build_result}") +endif() +execute_process(COMMAND "${CMAKE_COMMAND}" -E tar tf "${adminive_zip}" OUTPUT_VARIABLE archive_list RESULT_VARIABLE list_result) +if(NOT list_result EQUAL 0) + message(FATAL_ERROR "package_zip listing failed: ${list_result}") +endif() +string(REPLACE "\r\n" "\n" archive_list "${archive_list}") +string(REPLACE "\n" ";" archive_entries "${archive_list}") +set(root_cmake_count 0) +set(root_readme_count 0) +set(root_gitignore_count 0) +foreach(entry IN LISTS archive_entries) + if(entry MATCHES "^Adminive/") + message(FATAL_ERROR "package_zip contains nested stale project entry: ${entry}") + endif() + if(entry STREQUAL "CMakeLists.txt") + math(EXPR root_cmake_count "${root_cmake_count} + 1") + elseif(entry STREQUAL "README.md") + math(EXPR root_readme_count "${root_readme_count} + 1") + elseif(entry STREQUAL ".gitignore") + math(EXPR root_gitignore_count "${root_gitignore_count} + 1") + elseif(entry STREQUAL "backend/library/include/adminive/synchronized.hpp" OR entry STREQUAL "backend/service/tests/synchronized_test.cpp") + message(FATAL_ERROR "package_zip contains removed synchronized implementation: ${entry}") + endif() +endforeach() +if(NOT root_cmake_count EQUAL 1 OR NOT root_readme_count EQUAL 1 OR NOT root_gitignore_count EQUAL 1) + message(FATAL_ERROR "package_zip root exact-file selection is invalid") +endif() diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6559449..4bfaa95 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -152,7 +152,7 @@ function CollectionLab({view, items}: {view: unknown; items: unknown}) { {id: 'item-status', title: 'Item Status', method: 'GET' as const, url: `/admin/gallery/items/${first_id}/status`}, {id: 'overview-status', title: 'Overview Status', method: 'GET' as const, url: '/admin/status'}, {id: 'invalid-sort', title: `非法排序 ${invalid_sort}`, method: 'GET' as const, url: `/admin/gallery/items?orderBy=${encodeURIComponent(invalid_sort)}&orderDir=asc`}, - {id: 'invalid-filter', title: '非法筛选字段', method: 'GET' as const, url: '/admin/gallery/lab/invalid-filter'} + {id: 'invalid-filter', title: '非法筛选字段', method: 'GET' as const, url: '/admin/gallery/items?score=92'} ]; return

Collection 请求实验

按钮来自当前 Collection View 的 columns 能力,不在 React 里维护 Gallery_Row 字段清单。

{actions.map(action => )}
; } diff --git a/install_consumer/CMakeLists.txt b/install_consumer/CMakeLists.txt index ded4bef..a6c2528 100644 --- a/install_consumer/CMakeLists.txt +++ b/install_consumer/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.20) project(AdminiveInstallConsumer LANGUAGES CXX) -find_package(Adminive CONFIG REQUIRED COMPONENTS Core) +find_package(Adminive CONFIG REQUIRED COMPONENTS Core Default) add_executable(consumer main.cpp) target_link_libraries(consumer PRIVATE Adminive::Core) target_compile_features(consumer PRIVATE cxx_std_20) +add_executable(default_consumer default_main.cpp) +target_link_libraries(default_consumer PRIVATE Adminive::Default) +target_compile_features(default_consumer PRIVATE cxx_std_20) diff --git a/install_consumer/default_main.cpp b/install_consumer/default_main.cpp new file mode 100644 index 0000000..5c9e454 --- /dev/null +++ b/install_consumer/default_main.cpp @@ -0,0 +1,7 @@ +#include +#include +int main() { + httplib::Headers headers{{"X-Adminive", "installed"}}; + nlohmann::json value{{"status", 0}, {"transport", "httplib"}}; + return headers.empty() || adminive::dump_json(value).empty() ? 1 : 0; +} diff --git a/scripts/verify.py b/scripts/verify.py index 7079dc2..bff66c3 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -100,13 +100,20 @@ def full_verification() -> None: configure(release_dir, "Release") build(release_dir, "Release") ctest(release_dir, "Release") - configure(asan_dir, "Debug", ["-DADMINIVE_ENABLE_ASAN=ON", "-DADMINIVE_ENABLE_UBSAN=ON"]) - for target in SANITIZER_TARGETS: - build(asan_dir, "Debug", target) - ctest_regex(asan_dir, "Debug", SANITIZER_TARGETS) - configure(tsan_dir, "Debug", ["-DADMINIVE_ENABLE_TSAN=ON"]) - build(tsan_dir, "Debug", "Adminive_Managed_Test") - ctest(tsan_dir, "Debug", "concurrency") + if os.name == "nt": + configure(asan_dir, "Debug", ["-DADMINIVE_ENABLE_ASAN=ON"]) + for target in SANITIZER_TARGETS: + build(asan_dir, "Debug", target) + ctest_regex(asan_dir, "Debug", SANITIZER_TARGETS) + print("SKIP: UBSan and TSan verification require Clang or GNU and are not reported as passed on Windows MSVC", flush=True) + else: + configure(asan_dir, "Debug", ["-DADMINIVE_ENABLE_ASAN=ON", "-DADMINIVE_ENABLE_UBSAN=ON"]) + for target in SANITIZER_TARGETS: + build(asan_dir, "Debug", target) + ctest_regex(asan_dir, "Debug", SANITIZER_TARGETS) + configure(tsan_dir, "Debug", ["-DADMINIVE_ENABLE_TSAN=ON"]) + build(tsan_dir, "Debug", "Adminive_Managed_Test") + ctest(tsan_dir, "Debug", "concurrency") prepare_frontend() run_e2e(release_dir, "Release", False) if os.name == "nt":