Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- `cuke::current_feature()`, `cuke::current_scenario()` and `cuke::current_step()` to access the feature/scenario/step currently being executed from hooks or step definitions ([125](https://github.com/ThoSe1990/cwt-cucumber/pull/125))

### Fixed

- Literal parentheses `()` and curly braces `{}` in step text are not matched; escape them with `\(`, `\)`, `\{`, `\}` to use them literally instead of as optional text or a parameter type ([129](https://github.com/ThoSe1990/cwt-cucumber/pull/129))

## [2.9] 2026-06-26

### Added
Expand Down
7 changes: 7 additions & 0 deletions examples/step_definition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ WHEN(add_item, "I place {int} x {string} in it")

cuke::context<box>().add_items(item, count);
}
WHEN(coords, "I have {int},{int} as coordinates")
{
const int x = CUKE_ARG(1);
const int y = CUKE_ARG(2);

std::cout << "given coordinates x=" << x << " y=" << y << '\n';
}
WHEN(add_table_raw, "I add all items with the raw function:")
{
const cuke::table& t = CUKE_TABLE();
Expand Down
161 changes: 161 additions & 0 deletions gtest/step_finder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,17 @@ TEST(step_finder, multiple_values)
EXPECT_EQ(sf.values().at(5).as<short>(), 6);
}

TEST(step_finder, ints_in_parenthesis)
{
auto [pattern, types] =
create_regex_definition("these are \\({int},{int}\\) coordinates");
step_finder sf("these are (1,2) coordinates");
ASSERT_TRUE(sf.step_matches(pattern));
EXPECT_EQ(sf.values().size(), 2);
EXPECT_EQ(sf.values().at(0).as<int>(), 1);
EXPECT_EQ(sf.values().at(1).as<int>(), 2);
}

TEST(step_finder, word_at_stepend)
{
auto [pattern, types] = create_regex_definition("step with {word}");
Expand Down Expand Up @@ -495,6 +506,156 @@ TEST(step_finder, step_alternation_2)
}
}

TEST(step_finder, escaped_literal_parenthesis)
{
auto [pattern, types] =
create_regex_definition("this is a literal \\(parenthesis\\)");
step_finder sf("this is a literal (parenthesis)");
EXPECT_TRUE(sf.step_matches(pattern));
}

TEST(step_finder, escaped_parenthesis_do_not_match_without_them)
{
auto [pattern, types] =
create_regex_definition("this is a literal \\(parenthesis\\)");
step_finder sf("this is a literal parenthesis");
EXPECT_FALSE(sf.step_matches(pattern));
}

TEST(step_finder, mixed_escaped_and_optional_parenthesis)
{
auto [pattern, types] =
create_regex_definition("these are \\({int},{int}\\) coordinate(s)");
{
step_finder sf("these are (1,2) coordinate");
ASSERT_TRUE(sf.step_matches(pattern));
ASSERT_EQ(sf.values().size(), 2);
EXPECT_EQ(sf.values().at(0).as<int>(), 1);
EXPECT_EQ(sf.values().at(1).as<int>(), 2);
}
{
step_finder sf("these are (3,4) coordinates");
ASSERT_TRUE(sf.step_matches(pattern));
ASSERT_EQ(sf.values().size(), 2);
EXPECT_EQ(sf.values().at(0).as<int>(), 3);
EXPECT_EQ(sf.values().at(1).as<int>(), 4);
}
}

TEST(step_finder, escaped_literal_curly_braces)
{
auto [pattern, types] =
create_regex_definition("I see \\{foo\\} in the output");
step_finder sf("I see {foo} in the output");
EXPECT_TRUE(sf.step_matches(pattern));
}

TEST(step_finder, escaped_curly_braces_do_not_match_without_them)
{
auto [pattern, types] =
create_regex_definition("I see \\{foo\\} in the output");
step_finder sf("I see foo in the output");
EXPECT_FALSE(sf.step_matches(pattern));
}

TEST(step_finder, escaped_curly_braces_are_not_treated_as_a_type)
{
auto [pattern, types] = create_regex_definition("value is \\{5\\}");
step_finder sf("value is {5}");
ASSERT_TRUE(sf.step_matches(pattern));
EXPECT_EQ(sf.values().size(), 0);
}

TEST(step_finder, mixed_escaped_braces_and_real_type)
{
auto [pattern, types] =
create_regex_definition("I see \\{foo\\} and {int} items");
step_finder sf("I see {foo} and 3 items");
ASSERT_TRUE(sf.step_matches(pattern));
ASSERT_EQ(sf.values().size(), 1);
EXPECT_EQ(sf.values().at(0).as<int>(), 3);
}

TEST(step_finder, escaped_brace_wraps_a_real_type)
{
auto [pattern, types] = create_regex_definition("value is \\{{int}\\}");
step_finder sf("value is {5}");
ASSERT_TRUE(sf.step_matches(pattern));
ASSERT_EQ(sf.values().size(), 1);
EXPECT_EQ(sf.values().at(0).as<int>(), 5);
}

TEST(step_finder, multiple_separate_escaped_parenthesis_groups)
{
auto [pattern, types] = create_regex_definition("\\(a\\) and \\(b\\)");
step_finder sf("(a) and (b)");
EXPECT_TRUE(sf.step_matches(pattern));
}

TEST(step_finder, consecutive_escaped_parens_and_braces)
{
auto [pattern, types] = create_regex_definition("\\(\\)\\{\\}");
step_finder sf("(){}");
EXPECT_TRUE(sf.step_matches(pattern));
}

TEST(step_finder, escaped_parenthesis_at_string_start_and_end)
{
auto [pattern, types] = create_regex_definition("\\(start and end\\)");
{
step_finder sf("(start and end)");
EXPECT_TRUE(sf.step_matches(pattern));
}
{
step_finder sf("start and end");
EXPECT_FALSE(sf.step_matches(pattern));
}
}

TEST(step_finder, escaped_parenthesis_next_to_slash_alternation)
{
auto [pattern, types] = create_regex_definition("\\(cat/dog\\) is a pet");
{
step_finder sf("(cat) is a pet");
EXPECT_TRUE(sf.step_matches(pattern));
}
{
step_finder sf("(dog) is a pet");
EXPECT_TRUE(sf.step_matches(pattern));
}
{
step_finder sf("(cat/dog) is a pet");
EXPECT_FALSE(sf.step_matches(pattern));
}
}

TEST(step_finder, escaped_brace_combined_with_optional_text)
{
auto [pattern, types] =
create_regex_definition("The result(s) show \\{status\\}");
{
step_finder sf("The result show {status}");
EXPECT_TRUE(sf.step_matches(pattern));
}
{
step_finder sf("The results show {status}");
EXPECT_TRUE(sf.step_matches(pattern));
}
{
// missing literal braces must not match
step_finder sf("The result show status");
EXPECT_FALSE(sf.step_matches(pattern));
}
}

TEST(step_finder, unrelated_backslash_is_still_escaped_normally)
{
auto [pattern, types] =
create_regex_definition(add_escape_chars(R"(The path is C:\Users)"));
step_finder sf(R"(The path is C:\Users)");
EXPECT_TRUE(sf.step_matches(pattern));
}

class custom_types : public ::testing::Test
{
protected:
Expand Down
44 changes: 40 additions & 4 deletions src/util_regex.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,34 @@ namespace cuke::internal
[[nodiscard]] static std::string create_word_alternation(
const std::string& step)
{
std::string result = step;
std::string result;
result.reserve(step.size());

result = std::regex_replace(result, std::regex("\\)"), ")?");
result = std::regex_replace(result, std::regex("\\("), "(?:");
for (std::size_t i = 0; i < step.size(); ++i)
{
const char c = step[i];

if (c == '\\' && i + 1 < step.size() &&
(step[i + 1] == '(' || step[i + 1] == ')' || step[i + 1] == '{' ||
step[i + 1] == '}'))
{
result += '\\';
result += step[i + 1];
++i;
}
else if (c == '(')
{
result += "(?:";
}
else if (c == ')')
{
result += ")?";
}
else
{
result += c;
}
}

std::regex pattern("(\\w+)/(\\w+)");
std::smatch match;
Expand Down Expand Up @@ -91,8 +115,20 @@ static std::string add_escape_chars(const std::string& input)
/* '|' */};

std::string result;
for (char c : input)
for (std::size_t i = 0; i < input.size(); ++i)
{
const char c = input[i];

if (c == '\\' && i + 1 < input.size() &&
(input[i + 1] == '(' || input[i + 1] == ')' || input[i + 1] == '{' ||
input[i + 1] == '}'))
{
result += c;
result += input[i + 1];
++i;
continue;
}

if (special_chars.find(c) != special_chars.end())
{
result += '\\';
Expand Down
1 change: 1 addition & 0 deletions stress-tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ set(target stress-tests)

add_executable(${target}
${CMAKE_CURRENT_SOURCE_DIR}/step_definition.cpp
${CMAKE_CURRENT_SOURCE_DIR}/hooks.cpp
)
install(
TARGETS ${target}
Expand Down
20 changes: 20 additions & 0 deletions stress-tests/features/stress-tests.feature
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,23 @@ Feature: Stress tests
| val1 | val2 | val3 |
| | "" | |
| | "" | |

Scenario: Literal parenthesis in step text
When There is a point at (3,4)
Then The point should be at coordinates 3 and 4

Scenario: Literal parenthesis in step text
When There is a point at (-3,-4)
Then The point should be at coordinates -3 and -4

Scenario: Singular optional text
When I have 1 item in stock
Then The stock count should be 1

Scenario: Plural optional text
When I have 5 items in stock
Then The stock count should be 5

Scenario: Literal curly braces in step text
When I see {status} in the raw output
Then The captured text should be "{status}"
43 changes: 43 additions & 0 deletions stress-tests/step_definition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,46 @@ WHEN(empty_cells_in_examples, "Some values {word}, {} and {string} are empty")
const std::string string_value = CUKE_ARG(3);
cuke::is_true(string_value.empty());
}

struct point
{
int x = 0;
int y = 0;
};

WHEN(point_at, "There is a point at \\({int},{int}\\)")
{
cuke::context<point>().x = CUKE_ARG(1);
cuke::context<point>().y = CUKE_ARG(2);
}

THEN(point_check, "The point should be at coordinates {int} and {int}")
{
const int expected_x = CUKE_ARG(1);
const int expected_y = CUKE_ARG(2);

cuke::equal(expected_x, cuke::context<point>().x);
cuke::equal(expected_y, cuke::context<point>().y);
}

WHEN(items_in_stock, "I have {int} item(s) in stock")
{
cuke::context<int>() = CUKE_ARG(1);
}

THEN(stock_count_check, "The stock count should be {int}")
{
const int expected = CUKE_ARG(1);
cuke::equal(expected, cuke::context<int>());
}

WHEN(raw_output, "I see \\{status\\} in the raw output")
{
cuke::context<std::string>() = "{status}";
}

THEN(raw_output_check, "The captured text should be {string}")
{
const std::string expected = CUKE_ARG(1);
cuke::equal(expected, cuke::context<std::string>());
}
Loading