修复bug
This commit is contained in:
Binary file not shown.
@@ -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=$<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=$<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}" "$<CONFIG>")
|
||||
endif()
|
||||
|
||||
@@ -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 包使用:
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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. 设计判断清单
|
||||
|
||||
|
||||
@@ -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 "$<$<CONFIG:Release>:-O1>")
|
||||
target_compile_options(Adminive_Config_Store_Test PRIVATE "$<$<CONFIG:Release>:-O1>")
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "adminive/collection.hpp"
|
||||
#include "adminive/http.hpp"
|
||||
#include "httplib.h"
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
@@ -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<T>();
|
||||
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)));
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <variant>
|
||||
namespace adminive::example {
|
||||
using Json = nlohmann::json;
|
||||
inline constexpr std::string_view radio_states_api = "/admin/radio_states";
|
||||
template <class T>
|
||||
using Http_Resource = adminive::Http_Resource<T, Json>;
|
||||
template <class T>
|
||||
|
||||
@@ -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<Gallery_Target, Json>::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<Gallery_Runtime_Config>; Resource_Transaction<Gallery_Runtime_Model>", 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<Gallery_Config>; value.resolved_synchronization(); value.member<&T::field>().write(...)", examples["managed_synchronization"], Json{{"transport", "Resource_Service<Managed_Value<T>>"}}, 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<Json>(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<Json>(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<Json>(protocol_boundaries()));
|
||||
});
|
||||
server.Get("/admin/gallery/lab/invalid-filter", [](const httplib::Request&, httplib::Response& response) {
|
||||
Collection_Service<Gallery_Row, Json> 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));
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"}};
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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()
|
||||
|
||||
+25
-22
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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 <div className="lab-grid"><article className="lab-controls"><h3>Collection 请求实验</h3><p>按钮来自当前 Collection View 的 columns 能力,不在 React 里维护 Gallery_Row 字段清单。</p><div className="lab-actions">{actions.map(action => <button type="button" key={action.id} disabled={Boolean(running)} onClick={() => void run(action.id, action.method, action.url, action.body)}>{running === action.id ? '执行中…' : action.title}</button>)}<button type="button" onClick={run_column_reorder} disabled={schema.column_reorderable === false}>Column Reorder</button></div></article><JsonPanel title="当前请求 / HTTP 状态 / 后端响应" value={probe ?? {hint: '点击左侧动作执行真实 HTTP 请求'}} open /></div>;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#include <adminive/adapters/httplib.hpp>
|
||||
#include <adminive/adapters/nlohmann_json.hpp>
|
||||
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;
|
||||
}
|
||||
+14
-7
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user