diff --git a/CMakeLists.txt b/CMakeLists.txt index 209556a..e31e206 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,6 +112,12 @@ if(LITEARM_BUILD_TESTS) get_filename_component(name ${src} NAME_WE) add_executable(${name} ${src}) target_link_libraries(${name} PRIVATE litearm_test_support) + # test_example_args 验 examples/_common.cpp 的参数解析 ⇒ 把那份源码直接编进来。 + # 不依赖 LITEARM_BUILD_EXAMPLES (打包/交叉构建里 examples 可能是 OFF, 那时 + # 配置仍必须成功, 故不走 litearm_examples_common 这个 target)。 + if(name STREQUAL "test_example_args") + target_sources(${name} PRIVATE examples/_common.cpp) + endif() add_test(NAME ${name} COMMAND ${name}) # 有用例 SKIP 时框架返回 77 —— 让 ctest 报 `***Skipped` 而不是 "Passed"。 # ⚠ 这不是洁癖: ctest 默认**吞掉通过用例的全部输出**, 于是"11 个用例被跳过"在 diff --git a/examples/_common.cpp b/examples/_common.cpp index bf268aa..2e1000b 100644 --- a/examples/_common.cpp +++ b/examples/_common.cpp @@ -13,6 +13,16 @@ bool is_known_switch(const std::string& a) { return a == "--port" || a == "--go" || a == "--speed" || a == "-h" || a == "--help"; } +/// 整串能否解析成一个数 —— 负数 (如 `-0.25`) 是**参数**, 不是开关。 +/// 只看"开头是 `-`"会把 `./02_movej --go 0.1 0 -0.1 0 0 0 0` (02 头注释里的用法) +/// 整个挡死; `--dist -0.04` 同理 (实测踩过)。 +bool parses_as_number(const std::string& a) { + if (a.empty()) return false; + char* end = nullptr; + std::strtod(a.c_str(), &end); + return end != a.c_str() && *end == '\0'; +} + } // namespace bool parse_args(int argc, char** argv, const std::string& desc, Args* out) { @@ -40,10 +50,10 @@ bool parse_args(int argc, char** argv, const std::string& desc, Args* out) { std::string val = "1"; if (i + 1 < argc) { const std::string nxt = argv[i + 1]; - if (!nxt.empty() && nxt[0] != '-') val = argv[++i]; + if (!nxt.empty() && (nxt[0] != '-' || parses_as_number(nxt))) val = argv[++i]; } out->extra[key] = val; - } else if (!a.empty() && a[0] == '-' && !is_known_switch(a)) { + } else if (!a.empty() && a[0] == '-' && !is_known_switch(a) && !parses_as_number(a)) { std::cerr << "未知开关: " << a << " (用 --help 看用法)\n"; std::exit(2); } else { diff --git a/tests/test_example_args.cpp b/tests/test_example_args.cpp new file mode 100644 index 0000000..3601b78 --- /dev/null +++ b/tests/test_example_args.cpp @@ -0,0 +1,90 @@ +// 样例参数解析 (examples/_common.cpp) —— 负数位置参数与私有开关的负值。 +// +// 起因: 解析器原先把所有 `-` 开头的 token 都当未知开关 (exit 2), 于是 02_movej +// 头注释里推荐的 `./02_movej --go 0.1 0 -0.1 0 0 0 0` 根本跑不起来; +// `--dist -0.04` (向 -X 平移) 同理。修复口径: 能解析成**完整一个数**的 token +// (位置参数槽与私有开关的值槽) 按参数走; 非数字的 `-x` 仍是错误路径。 +#include "test_support.hpp" + +#include "../examples/_common.hpp" + +#ifndef _WIN32 +#include +#include +#endif + +namespace { + +/// 把**参数词表**喂给 example::parse_args (它从 argv[1] 开始读)。 +/// ⚠ 别把"程序名" (`02_movej`) 放进词表: 真实进程里程序名在 argv[0], 而 parse_args +/// 从 argv[1] 起解析 —— 放进词表会被 `atof` 吃成一个数字位置参数 (实测踩过)。 +bool parse(const std::vector& words, example::Args* out) { + std::vector store = words; + std::vector argv; + argv.reserve(store.size() + 1); + argv.push_back(const_cast("example")); + for (auto& w : store) argv.push_back(w.data()); + return example::parse_args(int(argv.size()), argv.data(), "test", out); +} + +} // namespace + +TEST(example_args_negative_positional_is_a_number_not_a_switch) { + // 02_movej 文档用法: 负数关节角必须能作为位置参数传进来。 + example::Args a; + CHECK(parse({"--go", "0.1", "0", "-0.1", "0", "0", "0", "0"}, &a)); + CHECK_TRUE(a.go); + CHECK_EQ(a.rest.size(), size_t(7)); + CHECK_NEAR(a.rest[0], 0.1, 1e-12); + CHECK_NEAR(a.rest[2], -0.1, 1e-12); +} + +TEST(example_args_single_dash_and_scientific_forms) { + example::Args a; + CHECK(parse({"-5", "-.5", "-1e-3"}, &a)); + CHECK_EQ(a.rest.size(), size_t(3)); + CHECK_NEAR(a.rest[0], -5.0, 1e-12); + CHECK_NEAR(a.rest[1], -0.5, 1e-12); + CHECK_NEAR(a.rest[2], -1e-3, 1e-15); +} + +TEST(example_args_private_switch_accepts_negative_value) { + // `--dist -0.04` (向 -X 平移) 要把 -0.04 吃成值, 而不是当成未知开关。 + example::Args a; + CHECK(parse({"--dist", "-0.04"}, &a)); + CHECK_EQ(a.extra.count("dist"), size_t(1)); + CHECK_NEAR(std::atof(a.extra.at("dist").c_str()), -0.04, 1e-12); + CHECK_TRUE(a.rest.empty()); +} + +TEST(example_args_positive_forms_unchanged) { + example::Args a; + CHECK(parse({"--dist", "0.04", "--verbose", "--tag", "trace_x"}, &a)); + CHECK_NEAR(std::atof(a.extra.at("dist").c_str()), 0.04, 1e-12); + CHECK_EQ(a.extra.at("verbose"), "1"); + CHECK_EQ(a.extra.at("tag"), "trace_x"); +} + +TEST(example_args_help_returns_false) { + example::Args a; + CHECK_FALSE(parse({"--help"}, &a)); + CHECK_FALSE(parse({"-h"}, &a)); +} + +#ifndef _WIN32 +TEST(example_args_unknown_switch_still_exits_2) { + // 非数字的 `-x` 仍是错误路径 (exit 2) —— 修复只放行"能解析成数"的 token。 + ::fflush(nullptr); + const pid_t pid = ::fork(); + CHECK(pid >= 0); + if (pid == 0) { + example::Args a; + parse({"-x"}, &a); // 期望 exit(2) + ::_exit(0); // 没退: 退出码 0, 父进程判失败 + } + int status = 0; + ::waitpid(pid, &status, 0); + CHECK(WIFEXITED(status)); + CHECK_EQ(WEXITSTATUS(status), 2); +} +#endif