This file is the authoritative quick-reference for AI coding agents and new contributors. It covers build commands, project layout, architecture, APIs, and conventions. Read it before touching code.
All commands assume you are at the repository root.
# Install dependencies and generate the CMake toolchain
conan install . -of ./build --build missing
# Configure (no sanitizers — use for regular development)
cmake -S . -B ./build \
-DCMAKE_TOOLCHAIN_FILE=./build/conan_toolchain.cmake \
-DCUCUMBER_UNDEFINED_STEPS_ARE_A_FAILURE=OFF
# Build all targets
cmake --build ./build -j$(nproc)cmake --build ./build -j$(nproc)# Unit tests (fast, ~500 tests, no feature files needed)
./build/bin/unittests
# Run a specific test suite or test by name filter
./build/bin/unittests --gtest_filter="step_finder*"
./build/bin/unittests --gtest_filter="step_finder.word_empty"
# Integration / stress tests (runs .feature files against the stress-test binary)
./build/bin/stress-tests ./stress-tests
# Example binary (also used as a smoke test in CI)
./build/bin/example ./examples --exclude-file 11_manual_fails.feature| Option | Default | Effect |
|---|---|---|
CUCUMBER_BUILD_TESTS_AND_EXAMPLES |
ON |
Build unittests, example, stress-tests targets |
CUCUMBER_UNDEFINED_STEPS_ARE_A_FAILURE |
ON |
Final result is FAILED if any step has no definition. Set OFF in CI and agent runs |
cwt-cucumber/
├── src/ # Library source — the interpreter itself
│ ├── scanner.cpp/.hpp # Tokenises raw .feature file text → tokens
│ ├── lexer.cpp/.hpp # Advances scanner, provides token stream to parser
│ ├── parser.hpp # Recursive-descent parser → builds the AST
│ ├── ast.hpp/.cpp # AST node types: feature, scenario, step, …
│ ├── registry.hpp/.cpp # Global step registry + built-in cucumber expressions
│ ├── step_finder.hpp/.cpp # Matches feature step text against registered regex patterns
│ ├── test_runner.hpp/.cpp # AST visitor that executes matched steps
│ ├── util_regex.hpp # create_regex_definition(), replace_variables() for outlines
│ ├── stub_generator.hpp # --generate-steps: AST visitor that emits C++ stubs
│ ├── catalog.hpp/.cpp # --steps-catalog: prints registered step definitions
│ ├── options.hpp/.cpp # CLI argument parsing (program_args)
│ ├── cucumber.hpp/.cpp # Entry point: cwt_cucumber class, entry_point()
│ ├── defines.hpp # All public macros: GIVEN, WHEN, THEN, BEFORE, …
│ ├── asserts.hpp # cuke::equal, cuke::is_true, cuke::less, …
│ ├── context.hpp # cuke::context<T>: per-scenario shared state
│ ├── table.hpp/.cpp # cuke::table: DataTable access
│ ├── value.hpp # cuke::value: type-erased runtime value
│ ├── expression.hpp # expression struct: {pattern, type_info, callback}
│ ├── param_info.hpp # param_info: offset + type metadata for CUKE_ARG
│ └── …
├── gtest/ # Google Test unit tests
│ ├── step_finder.cc # Tests for pattern matching & value extraction
│ ├── run_scenarios.cc # Tests that parse+run inline Gherkin scripts
│ ├── ast.cc # Parser/AST tests
│ ├── registry.cc # Expression registration tests
│ ├── scanner*.cc # Scanner/language tests
│ └── CMakeLists.txt # Adds each .cc file explicitly to the unittests target
├── stress-tests/
│ ├── features/ # .feature files exercising edge cases end-to-end
│ └── step_definition.cpp # Step implementations for stress-test scenarios
├── examples/
│ ├── features/ # Documented example .feature files (1_…, 2_…, …)
│ ├── step_definition.cpp # Example step implementations
│ ├── custom_parameters.cpp
│ └── hooks.cpp
├── .github/workflows/
│ ├── copilot-setup-steps.yml # Pre-builds env for Copilot cloud agent
│ └── unittests.yml # CI: Linux (GCC 13 + Clang 17), Windows, macOS
└── .clang-format # Allman braces, 2-space indent, Google base style
There are two independent phases: step registration (at program startup) and feature execution (at runtime).
Each GIVEN / WHEN / THEN / STEP macro defines a file-scope struct
whose constructor runs before main() and calls
cuke::registry().push_step(step_definition(...)).
Inside the step_definition constructor (src/step.cpp), the definition
string is pre-processed once and stored:
"I place {int} x {string} in it"
│
▼
add_escape_chars() (util_regex.hpp)
Escapes regex special characters in the literal parts of the definition
(e.g. "." → "\.", "[" → "\["). Cucumber expression tokens are not escaped.
│
▼
create_regex_definition() (util_regex.hpp)
Replaces every {type} token with its registered regex pattern:
{int} → (-?\\d+)
{string} → "([^"]*)"
{word} → ([^\\s]*)
{} → (.*)
… etc.
Wraps the whole result in ^…$ anchors.
Also builds the param_info vector (offset + type per capture group).
│
▼
Stored in step_definition as:
m_regex_definition — the pre-compiled regex string e.g. "^I place (-?\\d+) x \"([^\"]*)\" in it$"
m_type_info — vector<param_info> mapping CUKE_ARG index → registry callback
All step definitions sit in the global registry (cuke::registry())
with their regex already built before any feature file is touched.
A .feature file flows through this pipeline:
.feature text
│
▼
Scanner (scanner.cpp)
Reads raw text, recognises keywords (Feature:, Scenario:, Given, …),
produces token_type tokens.
│
▼
Lexer (lexer.cpp)
Wraps Scanner, advances the token stream, provides lookahead to the Parser.
│
▼
Parser (parser.hpp — header-only, recursive-descent)
Builds an AST rooted at gherkin_document.
Entry points:
parser::parse_from_file(path)
parser::parse_script(string_view) ← used in unit tests
Traversal:
parser::for_each_scenario(node_visitor&)
│
▼
AST nodes (ast.hpp)
feature_node → scenario_node / scenario_outline_node → step_node
Each step_node holds: keyword, name (step text), file, line,
doc_string, data_table.
On construction, step_node calls step_finder::find() to locate a matching
step_definition in the registry and sets m_has_step_definition.
│
▼
step_finder (step_finder.cpp)
step_finder::find(begin, end)
— iterates every step_definition in the registry
— calls step_matches(step_definition.regex_string()) for each
step_finder::step_matches(pattern)
— calls std::regex_match(feature_step_text, match, pattern)
— the pattern is the pre-built m_regex_definition from Phase 1
— on success, extracts captured groups into value_array
│
▼
test_runner (test_runner.cpp) — implements node_visitor
visit(scenario_node) — runs hooks, background, then each step
visit(scenario_outline_node) — expands Examples rows via replace_variables(),
then runs each concrete scenario
Step execution: step_node::run() calls the registered callback with
value_array, param_info vector, doc_string, table.
│
▼
Results (test_results.hpp/cpp)
Aggregates pass/fail/skip/undefined counts per step and scenario.
Printed by cwt_cucumber::print_results().
For Scenario Outline, replace_variables() (src/util_regex.hpp) runs
before step matching. It substitutes <placeholder> tokens in the step
text with the current row's cell values. Empty cells are substituted with
"" so that {string} / {word} / {} expressions still produce a
regex match (callbacks strip "" back to an empty string).
Built-in expressions live in src/registry.hpp in the m_expressions.standard map:
| Expression | Regex pattern | C++ type |
|---|---|---|
{int} |
(-?\\d+) |
int |
{short} |
(-?\\d+) |
short |
{long} |
(-?\\d+) |
long |
{byte} |
(-?\\d+) |
char |
{float} |
(-?\\d*\\.?\\d+) |
float |
{double} |
(-?\\d*\\.?\\d+) |
double |
{word} |
([^\\s]*) |
std::string — empty cell → empty string |
{string} |
"([^"]*)" |
std::string — value without quotes |
{} |
(.*) |
std::string (anonymous, matches anything incl. empty) |
Custom expressions are registered with CUSTOM_PARAMETER and can span
multiple capturing groups in their regex pattern:
// examples/custom_parameters.cpp
CUSTOM_PARAMETER(custom_parameter_date, "{date}",
R"((\d{4})-(\d{2})-(\d{2}))", "shipping date")
{
date d;
d.year = CUKE_PARAM_ARG(1).as<int>(); // 1st capture group of the pattern
d.month = CUKE_PARAM_ARG(2).as<int>(); // 2nd capture group
d.day = CUKE_PARAM_ARG(3).as<int>(); // 3rd capture group
return d;
}
WHEN(ship_the_box, "The box gets shipped at {date}")
{
date shipping_date = CUKE_ARG(1); // framework calls the {date} callback
}Inside the callback body, CUKE_PARAM_ARG(n) (1-based) addresses the n-th
capturing group of this expression's own pattern. It receives an iterator
pointing to the first capture group of the match plus a count telling it
how many groups belong to it.
The key concern: if a custom expression uses N capturing groups instead
of 1, the regex match result has more entries than there are logical
parameters. CUKE_ARG(2) must skip over those extra groups to land on the
correct value.
create_regex_definition() (src/util_regex.hpp) solves this by building
a std::vector<param_info> alongside the regex string:
struct param_info {
std::size_t offset; // accumulated extra groups from all *preceding* params
std::size_t param_count; // number of capturing groups in *this* expression
std::string key; // e.g. "{date}"
std::string description;
};For each {type} token encountered left-to-right:
- Look up the expression's regex pattern.
- Count its capturing groups:
value_count = regex(pattern).mark_count(). - Push
param_info{ current_offset, value_count, key, … }. - Advance
offset += value_count - 1(extra groups beyond the 1 expected by a caller usingCUKE_ARG).
Concrete walk-through — step "Ships {date} and {int} items":
| # | Expression | Pattern | Groups | offset stored |
Running offset |
|---|---|---|---|---|---|
| 1 | {date} |
(\d{4})-(\d{2})-(\d{2}) |
3 | 0 | 0 + (3-1) = 2 |
| 2 | {int} |
(-?\\d+) |
1 | 2 | 2 + (1-1) = 2 |
Compiled regex: ^(\d{4})-(\d{2})-(\d{2}) and (-?\\d+) items$
After a match the value array is [year, month, day, n] (indices 0-3).
CUKE_ARG(1) → zero_based=0, actual index = 0 + offset[0] = 0 + 0 = 0
key = "{date}", count = 3
→ calls {date} callback with begin=values[0], count=3
CUKE_PARAM_ARG(1) = values[0] (year)
CUKE_PARAM_ARG(2) = values[1] (month)
CUKE_PARAM_ARG(3) = values[2] (day)
CUKE_ARG(2) → zero_based=1, actual index = 1 + offset[1] = 1 + 2 = 3
key = "{int}", count = 1
→ returns values[3] (n)
So user-facing CUKE_ARG indices are always 1, 2, 3 … regardless of how
many internal groups each expression uses.
Include src/cucumber.hpp (or cwt/cucumber.hpp in installed form).
GIVEN(unique_fn_name, "step definition text {int} with {string}")
{
const int n = CUKE_ARG(1); // 1-based index
const std::string s = CUKE_ARG(2);
cuke::equal(n, 42);
}
WHEN(fn, "step text") { /* … */ }
THEN(fn, "step text") { /* … */ }
STEP(fn, "step text") { /* … */ } // keyword-agnostic aliasunique_fn_name is a C++ identifier — must be unique across the translation
unit. It has no runtime significance.
const int n = CUKE_ARG(1); // auto-converts via expression callback
const std::string s = CUKE_ARG(2);
const double d = CUKE_ARG(3);The type is resolved at the call site by the expression callback registered for
that {type}. C++ auto deduction does not work — always specify the type.
WHEN(fn, "I have the following table:")
{
const cuke::table& t = CUKE_TABLE();
// Iterate all rows (returns table::row objects)
for (const cuke::table::row& row : t.raw())
{
std::string val = row[0].to_string();
int n = row[1].as<int>();
}
// Named columns (first row is header)
for (const cuke::table::row& row : t.hashes())
{
std::string item = row["ITEM"].to_string();
}
// Single key-value pair (two-column table, first row is key)
cuke::table::pair kv = t.rows_hash();
std::string item = kv["ITEM"].to_string();
}WHEN(fn, "I have a doc string:")
{
const std::string doc = CUKE_DOC_STRING(); // as single string
const std::vector<std::string> lines = CUKE_DOC_STRING(); // as lines
}A doc string's opening delimiter may carry a content type tag with no space
(e.g. ```json / """json). Access it with CUKE_DOC_STRING_TYPE() —
returns an empty string if no tag was given. The tag is metadata only: in a
Scenario Outline it is carried through unchanged, never substituted.
WHEN(fn, "I have a tagged doc string:")
{
std::string type = CUKE_DOC_STRING_TYPE(); // e.g. "json"
}In a Scenario Outline, doc string content is substituted like step text —
<placeholder> tokens matching an Examples column are replaced with the
row's value. Any other angle-bracket text (e.g. XML/HTML tags) is left
untouched instead of raising an error (replace_variables() in
src/util_regex.hpp, called with ignore_missing_key = true for doc
strings). A verbose-level log message is emitted for each ignored key.
cuke::context<T> is reset at the start of each scenario.
// Construct/reset T with optional constructor args
cuke::context<MyType>(); // default-construct
cuke::context<MyType>(arg1, arg2); // forwarded to T's constructor
// Access existing instance (reference)
MyType& obj = cuke::context<MyType>();
const MyType& obj = cuke::context<MyType>();All functions accept an optional std::string message as last argument.
cuke::equal(lhs, rhs);
cuke::not_equal(lhs, rhs);
cuke::greater(lhs, rhs);
cuke::greater_or_equal(lhs, rhs);
cuke::less(lhs, rhs);
cuke::less_or_equal(lhs, rhs);
cuke::is_true(condition);
cuke::is_false(condition);A failing assertion marks the current step and scenario as FAILED but does not throw — execution of the step body continues unless you return early.
BEFORE(fn) { /* runs before every scenario */ }
AFTER(fn) { /* runs after every scenario */ }
BEFORE_T(fn, "@tag") { /* conditional on tag expression */ }
AFTER_T(fn, "@tag") { /* conditional on tag expression */ }
BEFORE_STEP(fn) { /* runs before every step */ }
AFTER_STEP(fn) { /* runs after every step */ }
BEFORE_ALL(fn) { /* once before any scenario */ }
AFTER_ALL(fn) { /* once after all scenarios */ }
// Inside a hook body:
cuke::skip_scenario(); // mark scenario skipped, skip remaining steps
cuke::ignore_scenario(); // mark scenario ignored
cuke::fail_scenario("reason"); // immediately fail the scenario- Add an enum value to
program_args::arginsrc/options.hpp. - Add a
definitionentry to thedefs[]array in the same file. - Handle it in the appropriate method in
src/cucumber.cpp(e.g.export_catalog()for output-only flags, orrun_tests()for flags that affect the test run). - Add a unit test in
gtest/options.cc(or the closest relevant file).
Keywords are the Gherkin tokens a scanner recognises: Feature:,
Scenario:, Given, Examples:, etc. Each language has its own
identifier subclass in src/identifiers/.
scannerdefaults toenglishas its identifier.- If the feature file begins with
# language: de(ores, …) the scanner callsset_language()insrc/scanner.cpp, which swaps the identifier togerman/spanish. - At each position in the source the scanner calls
m_identifiers->get_token(str), which does a linearstarts_withscan of that language's keyword table and returns the matchingtoken_type.
token_type |
Gherkin concept |
|---|---|
feature |
Feature: header |
scenario |
Scenario: / Example: |
scenario_outline |
Scenario Outline: / Scenario Template: |
background |
Background: |
rule |
Rule: |
examples |
Examples: / Scenarios: |
step |
Given / When / Then / And / But / * |
Open src/identifiers/english.hpp (or german.hpp / spanish.hpp):
- Increment the
std::arraysize by the number of new entries. - Add a
{std::string_view("New Keyword:"), token_type::target_type}pair. Order does not matter for correctness, but put more specific keywords before shorter ones that share a prefix to avoid false matches.
Example — adding Scenario Template: and Scenarios: to English
(the exact change from PR #117):
// Before
static constexpr std::array<std::pair<std::string_view, token_type>, 12>
m_identifiers{{{std::string_view("Scenario Outline:"),
token_type::scenario_outline},
{std::string_view("Examples:"), token_type::examples},
/* … */}};
// After
static constexpr std::array<std::pair<std::string_view, token_type>, 14>
m_identifiers{{{std::string_view("Scenario Outline:"),
token_type::scenario_outline},
{std::string_view("Scenario Template:"), // ← new
token_type::scenario_outline},
{std::string_view("Examples:"), token_type::examples},
{std::string_view("Scenarios:"), // ← new
token_type::examples},
/* … */}};- Create
src/identifiers/mylang.hpp, subclassingidentifier:
#pragma once
#include <array>
#include "identifier.hpp"
namespace cuke::internal
{
class mylang : public identifier
{
std::pair<token_type, std::size_t> get_token(
std::string_view str) const override
{
for (const auto& element : m_identifiers)
{
if (str.starts_with(element.first))
{
return {element.second, element.first.length()};
}
}
return {token_type::none, 0};
}
private:
static constexpr std::array<std::pair<std::string_view, token_type>, N>
m_identifiers{{
{std::string_view("Feature:"), token_type::feature},
{std::string_view("Scenario:"), token_type::scenario},
// … all required keywords …
{std::string_view("Given"), token_type::step},
{std::string_view("When"), token_type::step},
{std::string_view("Then"), token_type::step},
{std::string_view("And"), token_type::step},
{std::string_view("But"), token_type::step},
}};
};
} // namespace cuke::internal- Register it in
src/scanner.cpp:
#include "identifiers/mylang.hpp"
void scanner::set_language(std::string_view country)
{
if (country == "de") { m_identifiers = std::make_shared<german>(); }
else if (country == "es") { m_identifiers = std::make_shared<spanish>(); }
else if (country == "xx") { m_identifiers = std::make_shared<mylang>(); } // ← add
}- Add unit tests in a new file
gtest/scanner_mylang.cc— oneTEST(mylang_keywords, …)per keyword, using the# language: xxheader:
TEST(mylang_keywords, feature)
{
const char* script = R"*(
# language: xx
<keyword here>
)*";
EXPECT_EQ(scanner(script).scan_token().type, token_type::feature);
}- Register the new
.ccfile ingtest/CMakeLists.txtby adding it to theadd_executablesource list.
Edit the m_expressions.standard map in src/registry.hpp:
{"{mytype}", {"(regex_pattern)", "mytype", make_parameter_value<CppType>}},If the C++ type needs custom conversion logic, supply a lambda instead of
make_parameter_value<T>:
{"{mytype}", {"(regex_pattern)", "mytype",
[](cuke::value_array::const_iterator begin, std::size_t count) -> any
{
return get_param_value(begin, count, 1).as<std::string>(); // example
}}},mark_count() of the regex pattern determines the offset arithmetic for
subsequent parameters (see the param_info section above).
Built-in expressions must have exactly one capturing group — this keeps
the offset contribution at zero and avoids surprising users. If you need
multiple groups, define a CUSTOM_PARAMETER instead.
Tests that verify a self-contained piece of logic (parser, step_finder, scanner, options, …).
Pattern 1 — pure logic, no registry:
// gtest/step_finder.cc
TEST(step_finder, my_new_test)
{
auto [pattern, types] = create_regex_definition("I have {int} items");
step_finder sf("I have 5 items");
ASSERT_TRUE(sf.step_matches(pattern));
ASSERT_EQ(sf.values().size(), 1);
EXPECT_EQ(sf.values().at(0).as<int>(), 5);
}Pattern 2 — full scenario execution with inline Gherkin:
// gtest/run_scenarios.cc (use a TEST_F with a SetUp that clears + registers)
class my_test : public ::testing::Test
{
protected:
void SetUp() override
{
cuke::registry().clear();
cuke::registry().push_step(cuke::internal::step_definition(
[](const cuke::value_array& v, const auto&, const auto&, const auto&)
{ /* assert on v */ },
"my step definition text"));
}
};
TEST_F(my_test, scenario_passes)
{
const char* script = R"*(
Feature: f
Scenario: s
Given my step definition text
)*";
cuke::parser p;
p.parse_script(script);
cuke::test_runner runner;
p.for_each_scenario(runner);
EXPECT_EQ(cuke::results::test_results().scenarios_passed(), 1);
}Registering a new .cc file: Add it to the add_executable list in
gtest/CMakeLists.txt.
The stress-tests/ directory is the home for edge-case and weird-structure
scenarios — things that are valid Gherkin but exercise tricky paths in the
interpreter that unit tests don't easily reach.
Use a unit test (gtest/) when… |
Use a stress test (stress-tests/) when… |
|---|---|
| Testing a single function / class in isolation | Testing the full pipeline end-to-end |
| No feature file involved | The bug manifests only when a .feature file is parsed and run |
| Fast, deterministic, easy to assert on return values | Edge-case Gherkin structures (empty cells, special chars, unusual formatting) |
stress-tests/
CMakeLists.txt — builds one executable linked to libcucumber
features/
stress-tests.feature — all stress scenarios live here (one file)
step_definition.cpp — step implementations for stress scenarios
hooks.cpp — hook examples (BEFORE/AFTER/BEFORE_STEP/etc.)
# build (from project root, after cmake configure)
cmake --build build --target stress-tests
# run all stress scenarios
./build/stress-tests/stress-tests ./stress-tests/features/stress-tests.feature-
Add a
ScenarioorScenario Outlinetostress-tests/features/stress-tests.feature.
Group related scenarios together and add a comment line if the intent is not obvious from the Gherkin alone. -
Implement any new steps in
stress-tests/step_definition.cpp.
Usecuke::is_true/cuke::equalto assert correctness inside steps — a failing assertion marks the scenario as failed. -
No CMake changes needed — all
.cppfiles in the directory are already compiled into thestress-teststarget. -
Run the binary and verify all scenarios pass before opening a PR.
# stress-tests/features/stress-tests.feature
Scenario Outline: special characters in word expressions
When The value is <val>
Then It should equal <expected>
Examples:
| val | expected |
| foo/bar | "foo/bar" |
| café | "café" |// stress-tests/step_definition.cpp
WHEN(special_word_given, "The value is {word}")
{
cuke::context<std::string>() = CUKE_ARG(1);
}
THEN(special_word_then, "It should equal {string}")
{
std::string expected = CUKE_ARG(1);
cuke::equal(expected, cuke::context<std::string>());
}| Scenario | What it exercises |
|---|---|
Scenario Outline: a scenario outline |
{word} + {} with numeric/punctuation values in outline cells |
Scenario Outline: lets put the quotes in the step |
Inline "…" delimiters in step text (not in cells) |
Scenario: Doc string with quotes |
"""…""" doc string parsing |
Scenario: Doc string with backticks |
```…``` doc string parsing |
Scenario: Doc string as vector |
CUKE_DOC_STRING() as std::vector<std::string> |
Scenario Outline: Doc string with XML content in a scenario outline |
Non-Examples angle-bracket text (XML tags) in a doc string left unchanged, while real <placeholder>s are substituted |
Scenario: Doc string with a content type tag / Scenario: Doc string without a content type tag |
CUKE_DOC_STRING_TYPE() with and without a ```json/"""json tag |
Scenario: Empty cells in data table |
CUKE_TABLE() with fully empty rows |
Scenario Outline: Empty cells in examples |
{word}, {}, {string} with empty outline cells ("" sentinel) |
From .clang-format:
- Base style: Google
- Allman braces — opening brace on its own line for every block
- Indent: 2 spaces (no tabs)
SortIncludes: false— keep include order as written- Max line length: 80 (Google default)
Run before committing:
clang-format -i src/*.hpp src/*.cpp gtest/*.ccCI will reject PRs that fail the format check.
Additional conventions observed in the codebase:
[[nodiscard]]on every function that returns a value that must be usednoexcepton getters and functions with no failure pathstaticon free functions that are file-local or header-only utilitiesconstreferences for all read-only parameters- No raw owning pointers — use
std::unique_ptror value semantics - Prefer
std::string_viewfor read-only string parameters;std::stringfor stored values - Use
std::format(C++20) for string formatting — do not useprintforostringstream
A release requires exactly two file changes.
Line 2 of the root CMakeLists.txt:
# Before
project(cwt-cucumber VERSION 2.8)
# After — example bump to 2.9
project(cwt-cucumber VERSION 2.9)CMake automatically propagates PROJECT_VERSION to the docs _version.py
file via the file(WRITE …) call on line 4.
Replace the ## [Unreleased] heading with the new version and today's date
(YYYY-MM-DD), then add a fresh empty ## [Unreleased] block above it:
# Changelog
## [Unreleased]
## [2.9] 2026-06-25 ← was [Unreleased], now stamped
### Added
- …previous unreleased entries stay here…
### Fixed
- …The date format is YYYY-MM-DD. The version number must match exactly what
is set in CMakeLists.txt.
The docs/ directory (Sphinx + Doxygen/Breathe) is built by CI / Read the
Docs, not locally. No local Sphinx/Doxygen build is necessary — do not
install sphinx-build, doxygen, or related tooling just to validate a
docs change; instead, review the .rst edits manually for correct syntax
(directives, indentation, .. _label: anchors and matching :ref: targets —
grep for the label name across docs/chapters/*.rst to confirm it's
referenced correctly).
Always update documentation for any feature or change that affects a
public API (new/changed macros such as GIVEN/WHEN/THEN/CUKE_ARG/
CUKE_DOC_STRING(), anything under the cuke:: namespace, CLI flags, or
Gherkin/keyword behavior). This includes, as relevant to the change:
docs/chapters/step_definitions.rstand/ordocs/chapters/cucumber_features.rst— user-facing usage and examplesdocs/chapters/api_reference.rst— add a.. doxygendefine::/.. doxygenfunction::entry for new public macros/functionsAGENTS.md— the relevant "Step Definition API" / architecture section, so this reference document stays accurateCHANGELOG.md— a one-liner under### Added/### Fixedin[Unreleased]
- Do not add mandatory dependencies. The library must remain buildable
with no external deps (
nlohmann_jsonis optional viaWITH_JSON). - Do not change
std::regex_matchback tostd::regex_searchinstep_finder.cpp— thewhileloop with(.*)patterns causes an infinite loop on empty suffixes. - Do not use
([^\s]+)or(.+)for built-in expressions that must support empty values — use([^\s]*)/(.*)and strip the""sentinel in the callback. - Do not add state to
step_finderthat persists across calls — it is constructed fresh per step lookup. - Do not use
std::cout/std::cerrdirectly in library code — route output throughcuke::log::info/cuke::log::error.