Skip to content

add cpp tag expressions#241

Open
hs515 wants to merge 13 commits intocucumber:mainfrom
hs515:cpp-tag-expressions
Open

add cpp tag expressions#241
hs515 wants to merge 13 commits intocucumber:mainfrom
hs515:cpp-tag-expressions

Conversation

@hs515
Copy link
Copy Markdown

@hs515 hs515 commented Jan 13, 2026

🤔 What's changed?

Add a CPP implementation of the Tag Expressions parser.

⚡️ What's your motivation?

This will allow CPP-based Cucumber runner to have a native tag expression parser.

🏷️ What kind of change is this?

  • ⚡ New feature (non-breaking change which adds new behaviour)

♻️ Anything particular you want feedback on?

  • The implementation is converted from the existing Python implementation.
  • Ln25 and Ln29 in testdata/parsing.yml are identical. Removing any of them is not included in this pull request.

📋 Checklist:

  • I agree to respect and uphold the Cucumber Community Code of Conduct
  • I've changed the behaviour of the code
    • I have added/updated tests to cover my changes.
  • My change requires a change to the documentation.
    • I have updated the documentation accordingly.
  • Users should know about my change
    • I have added an entry to the "Unreleased" section of the CHANGELOG, linking to this pull request.

This text was originally generated from a template, then edited by hand. You can modify the template here.

@hs515 hs515 marked this pull request as ready for review January 15, 2026 05:09
@mpkorstanje
Copy link
Copy Markdown
Member

@daantimmer your review would be welcome too.

Copy link
Copy Markdown
Member

@mpkorstanje mpkorstanje left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At a glance it looks like there are no tests against the testdata folder. To ensure all parsers work the same it would be preferable if there were.

For example see:

https://github.com/cucumber/tag-expressions/tree/main/python/tests/data

I think it would also allow some of hand written tests to be deleted.

@hs515
Copy link
Copy Markdown
Author

hs515 commented Jan 15, 2026

At a glance it looks like there are no tests against the testdata folder. To ensure all patsers work the same it would be preferable if there were.

For example see:

https://github.com/cucumber/tag-expressions/tree/main/python/tests/data

I think it would also allow some of hand written tests to be deleted.

Thanks for the comments. Tests against the testdata folder have been added under

https://github.com/hs515/tag-expressions/tree/cpp-tag-expressions/cpp/tests.

All listed tests passed. (Ln25 and Ln29 in testdata/parsing.yml are identical, so only test one.)

Running main() from ./googletest/src/gtest_main.cc
[==========] Running 46 tests from 3 test suites.
[----------] Global test environment set-up.
[----------] 17 tests from ErrorsTest
[ RUN ] ErrorsTest.ThrowsOnWrongOperatorUsage1
[ OK ] ErrorsTest.ThrowsOnWrongOperatorUsage1 (1 ms)
[ RUN ] ErrorsTest.ThrowsOnWrongOperatorUsage2
[ OK ] ErrorsTest.ThrowsOnWrongOperatorUsage2 (0 ms)
[ RUN ] ErrorsTest.ThrowsOnWrongOperatorUsage3
[ OK ] ErrorsTest.ThrowsOnWrongOperatorUsage3 (0 ms)
[ RUN ] ErrorsTest.ThrowsOnDoubleOperator1
[ OK ] ErrorsTest.ThrowsOnDoubleOperator1 (0 ms)
[ RUN ] ErrorsTest.ThrowsOnDoubleOperator2
[ OK ] ErrorsTest.ThrowsOnDoubleOperator2 (0 ms)
[ RUN ] ErrorsTest.ThrowsOnDoubleOperator3
[ OK ] ErrorsTest.ThrowsOnDoubleOperator3 (0 ms)
[ RUN ] ErrorsTest.ThrowsOnMissingOperatorBetweenTags
[ OK ] ErrorsTest.ThrowsOnMissingOperatorBetweenTags (0 ms)
[ RUN ] ErrorsTest.ThrowsOnUnbalancedCloseParentheses
[ OK ] ErrorsTest.ThrowsOnUnbalancedCloseParentheses (0 ms)
[ RUN ] ErrorsTest.ThrowsOnUnbalancedOpenParentheses
[ OK ] ErrorsTest.ThrowsOnUnbalancedOpenParentheses (0 ms)
[ RUN ] ErrorsTest.ThrowsOnEscapeRegularCharacter
[ OK ] ErrorsTest.ThrowsOnEscapeRegularCharacter (0 ms)
[ RUN ] ErrorsTest.ThrowsOnEscapeNothing
[ OK ] ErrorsTest.ThrowsOnEscapeNothing (0 ms)
[ RUN ] ErrorsTest.ThrowsOnAndMissingRightOperand
[ OK ] ErrorsTest.ThrowsOnAndMissingRightOperand (0 ms)
[ RUN ] ErrorsTest.ThrowsOnOrMissingRightOperand
[ OK ] ErrorsTest.ThrowsOnOrMissingRightOperand (0 ms)
[ RUN ] ErrorsTest.ThrowsOnNotMissingRightOperand1
[ OK ] ErrorsTest.ThrowsOnNotMissingRightOperand1 (0 ms)
[ RUN ] ErrorsTest.ThrowsOnNotMissingRightOperand2
[ OK ] ErrorsTest.ThrowsOnNotMissingRightOperand2 (0 ms)
[ RUN ] ErrorsTest.ThrowsOnAndMissingLeftOperand
[ OK ] ErrorsTest.ThrowsOnAndMissingLeftOperand (0 ms)
[ RUN ] ErrorsTest.ThrowsOnOrMissingLeftOperand
[ OK ] ErrorsTest.ThrowsOnOrMissingLeftOperand (0 ms)
[----------] 17 tests from ErrorsTest (1 ms total)

[----------] 7 tests from EvaluationsTest
[ RUN ] EvaluationsTest.EmptyTag
[ OK ] EvaluationsTest.EmptyTag (0 ms)
[ RUN ] EvaluationsTest.TagNotX
[ OK ] EvaluationsTest.TagNotX (0 ms)
[ RUN ] EvaluationsTest.TagXAndY
[ OK ] EvaluationsTest.TagXAndY (0 ms)
[ RUN ] EvaluationsTest.TagXOrY
[ OK ] EvaluationsTest.TagXOrY (0 ms)
[ RUN ] EvaluationsTest.EscapeParentheses
[ OK ] EvaluationsTest.EscapeParentheses (0 ms)
[ RUN ] EvaluationsTest.EscapeSpecialCharacters1
[ OK ] EvaluationsTest.EscapeSpecialCharacters1 (0 ms)
[ RUN ] EvaluationsTest.EscapeSpecialCharacters2
[ OK ] EvaluationsTest.EscapeSpecialCharacters2 (0 ms)
[----------] 7 tests from EvaluationsTest (0 ms total)

[----------] 22 tests from ParsingTest
[ RUN ] ParsingTest.EmptyTag
[ OK ] ParsingTest.EmptyTag (0 ms)
[ RUN ] ParsingTest.TagAandB
[ OK ] ParsingTest.TagAandB (0 ms)
[ RUN ] ParsingTest.TagAorB
[ OK ] ParsingTest.TagAorB (0 ms)
[ RUN ] ParsingTest.TagNotA
[ OK ] ParsingTest.TagNotA (0 ms)
[ RUN ] ParsingTest.TagAandBandC
[ OK ] ParsingTest.TagAandBandC (0 ms)
[ RUN ] ParsingTest.TagAandBorCandD
[ OK ] ParsingTest.TagAandBorCandD (0 ms)
[ RUN ] ParsingTest.TagNotAorBandNotCorNotDorEandF
[ OK ] ParsingTest.TagNotAorBandNotCorNotDorEandF (0 ms)
[ RUN ] ParsingTest.TagNotAorBandNotCorNotDorEandFWithEscapes
[ OK ] ParsingTest.TagNotAorBandNotCorNotDorEandFWithEscapes (0 ms)
[ RUN ] ParsingTest.TagNotAandB
[ OK ] ParsingTest.TagNotAandB (0 ms)
[ RUN ] ParsingTest.TagNotAorB
[ OK ] ParsingTest.TagNotAorB (0 ms)
[ RUN ] ParsingTest.TagNotAandBandCorNotDorF
[ OK ] ParsingTest.TagNotAandBandCorNotDorF (0 ms)
[ RUN ] ParsingTest.TagAEscapeandB
[ OK ] ParsingTest.TagAEscapeandB (0 ms)
[ RUN ] ParsingTest.TagEscapeAandB
[ OK ] ParsingTest.TagEscapeAandB (0 ms)
[ RUN ] ParsingTest.TagAandBEscape
[ OK ] ParsingTest.TagAandBEscape (0 ms)
[ RUN ] ParsingTest.TagAandBEscapeEscape
[ OK ] ParsingTest.TagAandBEscapeEscape (0 ms)
[ RUN ] ParsingTest.TagAEscapeEscapeandBEscapeEscape
[ OK ] ParsingTest.TagAEscapeEscapeandBEscapeEscape (0 ms)
[ RUN ] ParsingTest.TagAandEscapeB
[ OK ] ParsingTest.TagAandEscapeB (0 ms)
[ RUN ] ParsingTest.TagXorParenthesesY
[ OK ] ParsingTest.TagXorParenthesesY (0 ms)
[ RUN ] ParsingTest.TagXEscapeEscapeorYEscapeEscape
[ OK ] ParsingTest.TagXEscapeEscapeorYEscapeEscape (0 ms)
[ RUN ] ParsingTest.TagEscapeXorYEscapeorZEscape
[ OK ] ParsingTest.TagEscapeXorYEscapeorZEscape (0 ms)
[ RUN ] ParsingTest.TagXEscapeorYEscapeEscapeorZEscape
[ OK ] ParsingTest.TagXEscapeorYEscapeEscapeorZEscape (0 ms)
[ RUN ] ParsingTest.TagXEscapeorY
[ OK ] ParsingTest.TagXEscapeorY (0 ms)
[----------] 22 tests from ParsingTest (0 ms total)

[----------] Global test environment tear-down
[==========] 46 tests from 3 test suites ran. (2 ms total)
[ PASSED ] 46 tests.

@daantimmer
Copy link
Copy Markdown

daantimmer commented Jan 15, 2026

Interesting. Will have a look.

The reason why is that I've written exactly the same thing, but based on the JavaScript+python implementation.

Not only that, I've also written the whole runner part too, which I am now covering to a full messages based implementation.

Just so you know, I also have the cucumber expressions part ready ;-).

But if this is feature compete just as mine then great. Than I don't have to do the work to move my implementation here for the tag expression part.

I'll happily have a review on the implementation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is your reasoning on keeping some definitions in the header and some in the source? IMHO all but templates definitions should be in a source file.

@@ -0,0 +1,176 @@
#pragma once
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +8 to +9
namespace cucumber {
namespace tag_expressions {
Copy link
Copy Markdown

@daantimmer daantimmer Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since C++17 (that you are requiring) we have nested namespaces, see (8): https://en.cppreference.com/w/cpp/language/namespace.html

It'll make everything a tad more readable by having less indentations.

Suggested change
namespace cucumber {
namespace tag_expressions {
namespace cucumber::tag_expressions {

Don't forget to remove the extra curly brace at the end.

* @return true if expression evaluates to true with values
* @return false otherwise
*/
virtual bool evaluate(const std::set<std::string>& values) const = 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are not using SonarQube: SonarQube will give a warning on not adding an explicit compare functor. In general it suggests the following for all std::map and std::set:

Suggested change
virtual bool evaluate(const std::set<std::string>& values) const = 0;
virtual bool evaluate(const std::set<std::string, std::less<>>& values) const = 0;

Please follow SonarQube's advice

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using shared_ptr's is generally a design flaw. The same applies here. All of these shared_ptr's should in fact be unique_ptr's.

You'll never have shared ownership over a single instance of an Expression instance. Only a unique ownership.


std::string to_string() const override;

const std::string& name() const { return name_; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const std::string& name() const { return name_; }
std::string_view name() const { return name_; }

Comment on lines +83 to +100
explicit And(std::vector<std::shared_ptr<Expression>> terms)
: terms_(std::move(terms)) {}

bool evaluate(const std::set<std::string>& values) const override {
for (const auto& term : terms_) {
if (!term->evaluate(values)) {
return false; // SHORTCUT: Any false makes the expression false
}
}
return true; // OTHERWISE: All terms are true
}

std::string to_string() const override;

const std::vector<std::shared_ptr<Expression>>& terms() const { return terms_; }

private:
std::vector<std::shared_ptr<Expression>> terms_;
Copy link
Copy Markdown

@daantimmer daantimmer Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you took this implementation from Python. But it makes no sense that one can have more than two or less than two arguments to And and Or. I propose to change the constructors to take two arguments left and right:

Suggested change
explicit And(std::vector<std::shared_ptr<Expression>> terms)
: terms_(std::move(terms)) {}
bool evaluate(const std::set<std::string>& values) const override {
for (const auto& term : terms_) {
if (!term->evaluate(values)) {
return false; // SHORTCUT: Any false makes the expression false
}
}
return true; // OTHERWISE: All terms are true
}
std::string to_string() const override;
const std::vector<std::shared_ptr<Expression>>& terms() const { return terms_; }
private:
std::vector<std::shared_ptr<Expression>> terms_;
And(std::unique_ptr<Expression> left, std::unique_ptr<Expression> right)
: left{ std::move(left) }
, right{ std::move(right) }
bool evaluate(const std::set<std::string>& values) const override {
left->Evaluate(values) && right->Evaluate(values);
}
std::string to_string() const override;
std::pair<const Expression*, const Expression*> terms() const { return {left.get(), right.get()}; }
private:
std::unique_ptr<Expression> left;
std::unique_ptr<Expression> right;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same applies for Or ofc.

Comment on lines +164 to +165
bool evaluate(const std::set<std::string>& values) const override {
(void)values; // Unused parameter
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bool evaluate(const std::set<std::string>& values) const override {
(void)values; // Unused parameter
bool evaluate([[maybe_unused]] const std::set<std::string>& values) const override {

Comment on lines +18 to +19
explicit TagExpressionError(const std::string& message)
: std::runtime_error(message) {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
explicit TagExpressionError(const std::string& message)
: std::runtime_error(message) {}
using std::runtime_error::runtime_error;

Comment thread cpp/src/expression.cpp Outdated

} // namespace tag_expressions
} // namespace cucumber

No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Comment thread cpp/src/lib/parser.cpp
Comment on lines +27 to +29
bool TokenInfo::is_binary() const {
return keyword == "or" || keyword == "and";
}
Copy link
Copy Markdown

@daantimmer daantimmer Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is unused

Suggested change
bool TokenInfo::is_binary() const {
return keyword == "or" || keyword == "and";
}

: keyword(std::move(kw)), precedence(prec), assoc(a), token_type(tt) {}

bool is_operation() const { return token_type == TokenType::OPERATOR; }
bool is_binary() const;
Copy link
Copy Markdown

@daantimmer daantimmer Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is unused

Suggested change
bool is_binary() const;

Comment thread cpp/tests/README.md
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are missing the Precedence tests like( this is coming from my implementation ):

    TEST(TestToken, TestPrecedence)
    {
        EXPECT_THAT(OR.HasLowerPrecedenceThan(OR), testing::IsTrue());
        EXPECT_THAT(OR.HasLowerPrecedenceThan(AND), testing::IsTrue());
        EXPECT_THAT(OR.HasLowerPrecedenceThan(NOT), testing::IsTrue());
        EXPECT_THAT(AND.HasLowerPrecedenceThan(AND), testing::IsTrue());
        EXPECT_THAT(AND.HasLowerPrecedenceThan(OR), testing::IsFalse());
        EXPECT_THAT(AND.HasLowerPrecedenceThan(NOT), testing::IsTrue());
        EXPECT_THAT(NOT.HasLowerPrecedenceThan(NOT), testing::IsFalse());
        EXPECT_THAT(NOT.HasLowerPrecedenceThan(OR), testing::IsFalse());
        EXPECT_THAT(NOT.HasLowerPrecedenceThan(AND), testing::IsFalse());
    }

    TEST(TestToken, TestPrecedenceWithParenthesis)
    {
        EXPECT_THAT(OR.HasLowerPrecedenceThan(OPEN_PARENTHESIS), testing::IsFalse());
        EXPECT_THAT(OR.HasLowerPrecedenceThan(CLOSE_PARENTHESIS), testing::IsFalse());
        EXPECT_THAT(AND.HasLowerPrecedenceThan(OPEN_PARENTHESIS), testing::IsFalse());
        EXPECT_THAT(AND.HasLowerPrecedenceThan(CLOSE_PARENTHESIS), testing::IsFalse());
        EXPECT_THAT(NOT.HasLowerPrecedenceThan(OPEN_PARENTHESIS), testing::IsFalse());
        EXPECT_THAT(NOT.HasLowerPrecedenceThan(CLOSE_PARENTHESIS), testing::IsFalse());
    }

Comment thread cpp/src/parser.cpp Outdated
}
};

std::vector<Token> operations;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
std::vector<Token> operations;
std::stack<Token> operations;

Comment thread cpp/src/parser.cpp Outdated
throw TagExpressionError("Invalid expression: Expected exactly one result");
}

return expressions.back();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not entirely sure about this one:

Suggested change
return expressions.back();
return std::move(expressions.back());

Comment thread cpp/CMakeLists.txt Outdated
Comment on lines +10 to +14
if(MSVC)
add_compile_options(/W4 /WX)
else()
add_compile_options(-Wall -Wextra -Wpedantic -Werror)
endif()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only do this when building in standalone mode.

Leave additional warnings (and especially -Werror) off when being consumed as a project.

Comment thread cpp/CMakeLists.txt Outdated
target_link_libraries(example PRIVATE cucumber::tag-expressions)

# Enable testing
enable_testing()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't always enable_testing put that under a cmake options flag.

Comment thread cpp/CMakeLists.txt Outdated
Comment on lines +39 to +54
find_package(GTest QUIET)
if(GTest_FOUND)
add_executable(tag_expressions_test
tests/test_errors.cpp
tests/test_evaluations.cpp
tests/test_parsing.cpp
)
target_link_libraries(tag_expressions_test
PRIVATE
cucumber::tag-expressions
GTest::gtest
GTest::gtest_main
)

add_test(NAME tag_expressions_test COMMAND tag_expressions_test)
endif()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put all of these under that same cmake options flag to 'enable testing build'. For example this is what I do for my libraries:

option(CCR_BUILD_TESTS "Enable build of the tests" ${CCR_DEFAULTOPT})
option(CCR_ENABLE_COVERAGE "Enable compiler flags for code coverage measurements" Off)

@daantimmer
Copy link
Copy Markdown

daantimmer commented Jan 15, 2026

In general what I am missing are some good practices:

  • no static analyzers (cpplint, sonarqube, clang-tidy, clang-format to name a few)
  • no test coverage
  • no runtime analyzers (during a test build, ASAN, UBSAN)
  • Ensure, using CI tests, that the code works on:
    • GCC
    • clang
    • clang-cl (targeting winsdk)
    • MSVC

For the latter point, if you want, we (not cucumber, but Philips) have a container available for CI that allows building for all of the above targets (except for native windows, just use a windows github runner for that)

@daantimmer
Copy link
Copy Markdown

daantimmer commented Jan 15, 2026

@mpkorstanje besides my comments the functionality itself is functionally the same as what I did. There are some taste-differences in how to write C++, but give 10 engineers the same assignment and you get 10 different implementations :-).

I do think my comments are valid and require downstream changes.

What might be worth the effort is to add a yaml parser dependency for a test build (it's what I did). Which allowed me to straight up use the official testdata. Which means no unit tests need to be modified when the testdata is expanded/updated.

See: std::vectortesting::TestInfo* RegisterMyTests() as an example of how I used yaml-cpp to implement dynamic tests based on the testdata folder.

@daantimmer
Copy link
Copy Markdown

Also, @hs515 be sure to have a look here: https://github.com/philips-software/amp-cucumber-cpp-runner/tree/feature/rewrite-to-use-cucumber-messages as you don't have to recreate everything that I already have ;-)

*
* @param name Tag name to represent as a literal
*/
explicit Literal(std::string name) : name_(std::move(name)) {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to comment once for all initializers. Please use uniform initialization to initialize variables and members: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es23-prefer-the--initializer-syntax

Suggested change
explicit Literal(std::string name) : name_(std::move(name)) {}
explicit Literal(std::string name) : name_{std::move(name)} {}

Copy link
Copy Markdown

@daantimmer daantimmer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like your effort. Its functionally the same as what I already did. Minus the comments I've given.

However, there are, for me, some blocking comments that need to be resolved before this could be merged. Feel free to discuss them! Here on on the cucumber discord (there is no C++ channel yet)

@mpkorstanje
Copy link
Copy Markdown
Member

mpkorstanje commented Jan 15, 2026

@hs515 I do have a few more questions about your motivation for contributing. And I'm sorry I didn't lead with these questions, but Daans comments prompted to think about this.

  • Who are you and what is your background?
  • What is your personal motivation for contributing?
  • What project will you be using this for? Can you show examples? Note: We are currently in the process of consider the cucumber-cpp for replacement or deprecation. If you are using Cucumber CPP or are writing this for use with Cucumber CPP you'll want to comment here: Consider a merger with amp-cucumber-cpp-runner cucumber-cpp#313
  • Do you intent to contribute bug fixes and other issues going forward?
  • Have you used an LLM to write this code?

These question are a bit of a departure from the past practices, but with LLM assistance writing plausible looking code has become easy enough that the effort involved in writing that code is no longer a signal that the author had a genuine and sustained interest in contributing.

@daantimmer good points all. Cheers!

@hs515
Copy link
Copy Markdown
Author

hs515 commented Jan 16, 2026

I like your effort. Its functionally the same as what I already did. Minus the comments I've given.

However, there are, for me, some blocking comments that need to be resolved before this could be merged. Feel free to discuss them! Here on on the cucumber discord (there is no C++ channel yet)

@daantimmer, thank you very much for your decent review. I will address @mpkorstanje's questions and then come back to your comments. Many thanks.

@hs515
Copy link
Copy Markdown
Author

hs515 commented Jan 16, 2026

@hs515 I do have a few more questions about your motivation for contributing. And I'm sorry I didn't lead with these questions, but Daans comments prompted to think about this.

  • Who are you and what is your background?
  • What is your personal motivation for contributing?
  • What project will you be using this for? Can you show examples? Note: We are currently in the process of consider the cucumber-cpp for replacement or deprecation. If you are using Cucumber CPP or are writing this for use with Cucumber CPP you'll want to comment here: Consider a merger with amp-cucumber-cpp-runner cucumber-cpp#313
  • Do you intent to contribute bug fixes and other issues going forward?
  • Have you used an LLM to write this code?

These question are a bit of a departure from the past practices, but with LLM assistance writing plausible looking code has become easy enough that the effort involved in writing that code was a signal that the author had a genuine and sustained interest in contributing.

@daantimmer good points all. Cheers!

  • Who are you and what is your background?
    @mpkorstanje , I now work at GE HealthCare software. I have 20+ years C++ programming experience.

  • What is your personal motivation for contributing?
    My personal motivation for contributing is to benefit more people who may use cucumber-cpp in their real lives. In the meantime, I can also learn from others.

  • What project will you be using this for? Can you show examples?
    We have been using cucumber-cpp in our organization for years. The original Ruby wire solution is cumbersome. We have created a solution to eliminate Ruby by bridging cucumber/gherkin/c parser and cucumber-cpp (wire-protocol) in a native c++ solution. Recently, I am working on another GitHub project to improve that solution. Supporting modern tag expressions is one of the goals. Other goals include using cucumber/gherkin/cpp parser and generating pretty reports. Because, as an organization, we trust the Cucumber community as the formal maintainer of the Cucumber standard, we prefer a cucumber-repo-based solution (No offense to amp-cucumber-cpp-runner).

I have noticed the #313 merge request. I am not sure how you are going to react to that. But no matter to merge the whole thing or to break it down and merge it in parts, I think it makes sense to have a C++ tag-expressions implementation in the repo. If the community decides to merge the C++ tag-expressions from somewhere else, I can close my current merge request.

  • Do you intent to contribute bug fixes and other issues going forward?
    Yes, I am willing to contribute bug fixes or other issues.

  • Have you used an LLM to write this code?
    Yes, I have used GitHub Copilot when writing this code. The LLM can help programmers code more efficiently. Then why not?

Let me know whether I should continue addressing @daantimmer 's comments?

Thanks.

@mpkorstanje
Copy link
Copy Markdown
Member

Thanks for the clarification. Much appreciated!

With that in mind we'd be happy to host the cpp implementation of tag expressions.

I have noticed the #313 merge request. I am not sure how you are going to react to that.

Right now that will be for Urs Fässler to decide. We mostly seem to have a dearth people interested in the original cucumber-cpp so I can't imagine any tears will be shed if does get archived/merged/replaced.

Yes, I have used GitHub Copilot when writing this code. The LLM can help programmers code more efficiently. Then why not?

I've never found the writing part to be the bottleneck. Reviewing on the other hand is more tedious.

The project mostly runs on volunteers and I do not want to waste their time asking them to review unreviewed code. So I would ask that you've made sure to fully understand each line before it is contributed. Though with 20+ years of experience I'm sure you'll be able to pick out the generated mistakes quite quickly so that is less of a concern than it originally was.

@hs515
Copy link
Copy Markdown
Author

hs515 commented Jan 16, 2026

With that in mind we'd be happy to host the cpp implementation of tag expressions.

Great! Then I will start addressing Daan's comments.

Right now that will be for Urs Fässler to decide. We mostly seem to have a dearth people interested in the original cucumber-cpp so I can't imagine any tears will be shed if does get archived/merged/replaced..

Running tests is not difficult today. GTest and some other test frameworks are very easy to use. However, Cucumber-cpp does still have its own advantages, such as human-readable feature files, tagging requirements to scenarios, adding tests without recompilation, and generating reports. IMO, if an easy-to-use cucumber-cpp was in place, it would gain more interest.

@mpkorstanje
Copy link
Copy Markdown
Member

if an easy-to-use cucumber-cpp was in place, it would gain more interest.

Chicken, meet egg. 😃

@hs515
Copy link
Copy Markdown
Author

hs515 commented Jan 17, 2026

In general what I am missing are some good practices:

  • no static analyzers (cpplint, sonarqube, clang-tidy, clang-format to name a few)
  • no test coverage
  • no runtime analyzers (during a test build, ASAN, UBSAN)
  • Ensure, using CI tests, that the code works on:

I will look at these open questions.

  • GCC
  • clang
  • clang-cl (targeting winsdk)
  • MSVC

For the latter point, if you want, we (not cucumber, but Philips) have a container available for CI that allows building for all of the above targets (except for native windows, just use a windows github runner for that)

@daantimmer , do you mind sharing how to use your (Philips) container that allows building for the above targets?

@hs515 hs515 requested a review from daantimmer January 17, 2026 04:46
@hs515
Copy link
Copy Markdown
Author

hs515 commented Jan 17, 2026

Also, @hs515 be sure to have a look here: https://github.com/philips-software/amp-cucumber-cpp-runner/tree/feature/rewrite-to-use-cucumber-messages as you don't have to recreate everything that I already have ;-)

@daantimmer , amazing, you have done a great work! Why didn't you break your work down and create pull requests to the cucumber repo? I think that could be more visible.

@hs515
Copy link
Copy Markdown
Author

hs515 commented Jan 17, 2026

@mpkorstanje besides my comments the functionality itself is functionally the same as what I did. There are some taste-differences in how to write C++, but give 10 engineers the same assignment and you get 10 different implementations :-).

I do think my comments are valid and require downstream changes.

Thanks for your great comments. I am addressing as many as I can.

What might be worth the effort is to add a yaml parser dependency for a test build (it's what I did). Which allowed me to straight up use the official testdata. Which means no unit tests need to be modified when the testdata is expanded/updated.

See: std::vectortesting::TestInfo* RegisterMyTests() as an example of how I used yaml-cpp to implement dynamic tests based on the testdata folder.

For a standalone project, I would like to agree to add a yaml parser dependency. But for a project that is itself a dependency, my preference would be opposite (not to add unnecessary dependencies).

@daantimmer
Copy link
Copy Markdown

Also, @hs515 be sure to have a look here: https://github.com/philips-software/amp-cucumber-cpp-runner/tree/feature/rewrite-to-use-cucumber-messages as you don't have to recreate everything that I already have ;-)

@daantimmer , amazing, you have done a great work! Why didn't you break your work down and create pull requests to the cucumber repo? I think that could be more visible.

Multiple reasons:

  • in the beginning when I started I was not aware that cucumber would take custom implementations. I wasn't aware of their 100% voluntary model
  • working at Philips has its disadvantages: officially every public contribution that I do should be checked by our IP office. These rules are a bit relaxed when working on our own public repos
  • at first I wanted to do the same kind of model of req-and-roll
  • I only got to know the people behind cucumber a few months ago where I learned their model which triggered me positively to start decoupling my project so I can contribute sub-parts and most likely in the end the whole runner once its finished. I will still have the IP office to deal with
  • working at Philips has its benefits as well. Developing in our own repository/organization gives/gave me huge resources for CI
  • it being a Philips repository made it easier for my coworkers to start using it and contribute where necessary

My personal goal would be to get everything towards cucumber. Most of the separate functionalities are already decoupled for the most part, bar some "timestamp" and "duration" utility functions.

That branch that I shared is nearing completion. Some minor cleanups to the current formatters are required. And some documentation on how to use everything.

Only "major" things I am lacking so far:

  • no HTML output
  • no snippets generation for missing steps
  • regular expressions do not do argument type lookups for its captures. Which means I am currently limited to strings for regular expressions
  • no parallel execution

I'll need to write additional unit tests, add everything is currently only tested though the compatibility kit layer. Which gives me 85% coverage, but I want to be higher. Also, unit tests :-) (I had them for my first implementation, but when I rewrote based on cucumber-js it was easier to get the implementation in first)

Everything else is quite complete compared to cucumber-js. (Which I used as an architectural/design example).

@hs515
Copy link
Copy Markdown
Author

hs515 commented Jan 22, 2026

I copied the tag-expressions/cpp into another publish repo, and ran SonarQube analysis and coverage tests. Here is the result: https://sonarcloud.io/project/overview?id=hs515_cpp-tag-expressions.

I have ensured it runs in CI with GCC.

Next step, I will continue trying

  • clang
  • clang-cl (targeting winsdk)
  • MSVC

@mpkorstanje, should I or @daantimmer close review comments?

@hs515
Copy link
Copy Markdown
Author

hs515 commented Jan 22, 2026

I have another cucumber-cpp runner project that depends on the existing cucumber-cpp and cucumber-gherkin projects. The existing cucumber-cpp supports snippets generation and REGEX_PARAM argument type lookups, but the current cucumber-cpp does not support cucumber expressions.

My running project can also generate HTML outputs. You can take a look at https://github.com/hs515/cuke-cpp.

Only "major" things I am lacking so far:

  • no HTML output
  • no snippets generation for missing steps
  • regular expressions do not do argument type lookups for its captures. Which means I am currently limited to strings for regular expressions
  • no parallel execution

@daantimmer
Copy link
Copy Markdown

daantimmer commented Jan 22, 2026

@hs515 I'll take a look and steal it when possible. First things first though which is to wrap up the current PR. Don't want to feature creep it :-)

@hs515
Copy link
Copy Markdown
Author

hs515 commented Jan 24, 2026

Auto build and run CI passed for GCC, Clang, and MSVC. Build activities can be found at https://github.com/hs515/cpp-tag-expressions/actions/runs/21267166216.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants