From 70b498858346ff162c3160c4a542c7c9bef899f8 Mon Sep 17 00:00:00 2001 From: Griswald Brooks Date: Thu, 15 Oct 2026 15:15:02 -0400 Subject: [PATCH 1/2] fix(script): restore double-quoted string literals The 4.9 tokenizer only recognized single quotes, so existing Script and precondition expressions failed on their opening double quote. Recognize both delimiters and require the closing quote to match the opening one. Cover the reported Objective expressions, quote parity and malformed strings, and XML Script/precondition attributes using ". Refs https://github.com/PickNikRobotics/moveit_pro/issues/23851 --- src/script_tokenizer.cpp | 6 +-- tests/script_parser_test.cpp | 98 ++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/script_tokenizer.cpp b/src/script_tokenizer.cpp index 591f1e4ee..98e3c4174 100644 --- a/src/script_tokenizer.cpp +++ b/src/script_tokenizer.cpp @@ -239,11 +239,11 @@ std::vector tokenize(const std::string& source) const size_t start = i; - // Single-quoted string literal - if(c == '\'') + // String literal: the closing quote must match the opening quote. + if(c == '\'' || c == '"') { ++i; - while(i < len && source[i] != '\'') + while(i < len && source[i] != c) { ++i; } diff --git a/tests/script_parser_test.cpp b/tests/script_parser_test.cpp index 2e2c5559d..64ed0b1c2 100644 --- a/tests/script_parser_test.cpp +++ b/tests/script_parser_test.cpp @@ -85,6 +85,104 @@ TEST(ParserTest, AnyTypes) EXPECT_EQ(result.value().cast(), 0); } +TEST(ParserTest, StringLiteralQuotes) +{ + struct TestCase + { + const char* script; + const char* expected; + }; + const TestCase cases[] = { + { "'hello world '", "hello world " }, + { R"("hello world ")", "hello world " }, + { "''", "" }, + { R"("")", "" }, + { R"('say "hello"')", "say \"hello\"" }, + { R"("it's ready")", "it's ready" }, + { R"('path\to\file')", R"(path\to\file)" }, + { R"("path\to\file")", R"(path\to\file)" }, + { "'first\nsecond'", "first\nsecond" }, + { "\"first\nsecond\"", "first\nsecond" }, + { R"('single'.."double")", "singledouble" }, + }; + BT::Ast::Environment env = { BT::Blackboard::create(), {} }; + for(const auto& test : cases) + { + SCOPED_TRACE(test.script); + auto result = BT::ParseScriptAndExecute(env, test.script); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value().cast(), test.expected); + } + + EXPECT_FALSE(BT::ValidateScript("'unterminated")); + EXPECT_FALSE(BT::ValidateScript(R"("unterminated)")); + EXPECT_FALSE(BT::ValidateScript(R"("mismatched')")); + EXPECT_FALSE(BT::ValidateScript(R"('mismatched")")); +} + +TEST(ParserTest, DoubleQuotedObjectiveExpressions) +{ + BT::Ast::Environment env = { BT::Blackboard::create(), {} }; + env.vars->set("viz_idx", 3); + env.vars->set("index", 2); + env.vars->set("prompt", std::string("bottles")); + env.vars->set("tool_object_name", std::string("tool")); + env.vars->set("execute_trajectories", std::string("true")); + + EXPECT_EQ(GetScriptResult(env, R"(viz_marker_name:="place_pose_"..viz_idx)") + .cast(), + "place_pose_3"); + EXPECT_EQ(GetScriptResult(env, R"(mask_label:="mask "..index)").cast(), + "mask 2"); + EXPECT_EQ( + GetScriptResult(env, R"(header := "SAM3 prompt="..prompt)").cast(), + "SAM3 prompt=bottles"); + EXPECT_EQ(env.vars->get("viz_marker_name"), "place_pose_3"); + EXPECT_EQ(env.vars->get("mask_label"), "mask 2"); + EXPECT_EQ(env.vars->get("header"), "SAM3 prompt=bottles"); + + EXPECT_EQ(GetScriptResult(env, R"(tool_object_name!="")").cast(), 1); + EXPECT_EQ(GetScriptResult(env, R"(execute_trajectories == "true")").cast(), 1); + GetScriptResult(env, R"(tool_object_name=""; execute_trajectories="false")"); + EXPECT_EQ(GetScriptResult(env, R"(tool_object_name!="")").cast(), 0); + EXPECT_EQ(GetScriptResult(env, R"(execute_trajectories == "true")").cast(), 0); +} + +TEST(ParserTest, DoubleQuotedXmlScriptAndPreconditions) +{ + BT::BehaviorTreeFactory factory; + const std::string xml_text = R"( + + + +