Skip to content

test(bdd): add C++ SDK BDD tests for basic messaging#3554

Open
seokjin0414 wants to merge 3 commits into
apache:masterfrom
seokjin0414:2965-cpp-bdd-tests
Open

test(bdd): add C++ SDK BDD tests for basic messaging#3554
seokjin0414 wants to merge 3 commits into
apache:masterfrom
seokjin0414:2965-cpp-bdd-tests

Conversation

@seokjin0414

@seokjin0414 seokjin0414 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What

Adds BDD coverage for the C++ SDK by implementing the shared
bdd/scenarios/basic_messaging.feature with cucumber-cpp and wiring it into the
Docker-based BDD harness and CI, the same way the other SDKs run. This is the
second part of #2965; the messaging FFI it relies on landed in #3046.

How it works

cucumber-cpp only supports the Cucumber wire protocol, so the step definitions
(bdd/cpp/features/step_definitions) build with CMake into a wire server, and a
Ruby Cucumber runner (cucumber + cucumber-wire, pinned to the versions
cucumber-cpp itself tests against) drives the feature file over the wire against a
real iggy-server. The step logic and assertions are all C++ and exercise the
SDK's FFI (get_streams, create_stream, create_topic, send_messages,
poll_messages). The Iggy C++ SDK is built with cargo and linked into the wire
server as a staticlib.

Heads-up for reviewers

Because cucumber-cpp is wire-protocol-only, the C++ BDD image pulls in a Ruby
runtime (cucumber + cucumber-wire). Flagging this explicitly in case it
changes how you'd like the harness shaped.

Notes

Everything lives under bdd/cpp, next to the other SDK BDD suites; foreign/cpp
is unchanged apart from the reserve() fix to the existing test.

The TearDown fixture and the magic-number cleanup discussed on Discord are tracked
separately by @slbotbm.

Refs #2965

@seokjin0414 seokjin0414 marked this pull request as ready for review June 23, 2026 09:57
@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Jun 23, 2026
@slbotbm

slbotbm commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

@seokjin0414 the test should be in he root bdd/ folder, not inside of foreign/cpp

@slbotbm

slbotbm commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

ignore the above, i didn't read the description😅

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.08%. Comparing base (7257940) to head (78c3ba4).

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3554      +/-   ##
============================================
- Coverage     74.10%   74.08%   -0.03%     
  Complexity      937      937              
============================================
  Files          1258     1258              
  Lines        131485   131485              
  Branches     107354   107398      +44     
============================================
- Hits          97435    97405      -30     
+ Misses        30963    30941      -22     
- Partials       3087     3139      +52     
Components Coverage Δ
Rust Core 74.74% <ø> (+0.01%) ⬆️
Java SDK 62.44% <ø> (ø)
C# SDK 71.40% <ø> (-0.73%) ⬇️
Python SDK 88.88% <ø> (ø)
PHP SDK 84.29% <ø> (ø)
Node SDK 91.35% <ø> (ø)
Go SDK 40.14% <ø> (ø)
see 33 files with indirect coverage changes
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@seokjin0414

Copy link
Copy Markdown
Contributor Author

@slbotbm
no worries! ended up moving it to bdd/cpp anyway. i kept it out of foreign/cpp by building the wire server with CMake (FetchContent cucumber-cpp + the cxx staticlib) instead of Bazel, so it's self-contained like the other bdd dirs and
foreign/cpp now only carries the reserve() fix.
can switch it to a Bazel target under foreign/cpp if you'd prefer.

@slbotbm

slbotbm commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

@seokjin0414 I didn't understand why did you use cmake instead of bazel. I think it would be possible to use bazel in bdd/cpp. you could define iggy_cpp as a bazel dependency in bdd/cpp/MODULE.bazel, and then locally define the path to iggy_cpp as ../../foreign/cpp.

@slbotbm

slbotbm commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

/author

@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Jun 24, 2026
@seokjin0414

Copy link
Copy Markdown
Contributor Author

@slbotbm the cmake route was just to keep the test self-contained in bdd/cpp without touching foreign/cpp — i didn't realize local_path_override could pull the sdk in cleanly. it can, so i switched to bazel as you suggested: bdd/cpp is its own module now, cucumber-cpp from the registry + iggy_cpp via local_path_override(../../foreign/cpp). cmake is gone.

the only change to foreign/cpp is one line — making the iggy-cpp target public so the harness can depend on it. build + the full wire e2e (17 steps) pass.

@slbotbm slbotbm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's add a .clang-format as well that copies the one from foreign/cpp

You can use /ready when your PR is ready for review again

Comment thread bdd/cpp/BUILD.bazel
Comment thread bdd/cpp/Dockerfile Outdated
Comment thread foreign/cpp/BUILD.bazel
Comment thread bdd/cpp/.bazelrc Outdated
Comment thread bdd/cpp/.bazelrc Outdated
Comment thread bdd/cpp/MODULE.bazel Outdated
@slbotbm

slbotbm commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Let's also commit the ruby lockfile as well.

@seokjin0414 seokjin0414 force-pushed the 2965-cpp-bdd-tests branch 2 times, most recently from f168a4d to 023d49d Compare June 28, 2026 14:41
@seokjin0414

seokjin0414 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

@slbotbm
also addressed the two non-inline ones: added bdd/cpp/.clang-format (copied from foreign/cpp), and committed bdd/cpp/Gemfile.lock for the ruby side. rebased onto master — CI is green.

@seokjin0414

Copy link
Copy Markdown
Contributor Author

/ready

@seokjin0414 seokjin0414 requested a review from slbotbm June 28, 2026 15:45
@github-actions github-actions Bot added S-waiting-on-review PR is waiting on a reviewer and removed S-waiting-on-author PR is waiting on author response labels Jun 28, 2026
Comment thread bdd/cpp/Dockerfile
@seokjin0414

Copy link
Copy Markdown
Contributor Author

/ready

@seokjin0414 seokjin0414 requested a review from slbotbm July 1, 2026 00:39

@slbotbm slbotbm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some comments, otherwise looks lgtm

Comment thread bdd/cpp/Dockerfile Outdated

WORKDIR /workspace/bdd/cpp
COPY bdd/cpp/Gemfile bdd/cpp/Gemfile.lock ./
RUN gem install bundler --no-document \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's pin bundler to a specific version

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

pinned to 2.6.9 (matches BUNDLED WITH in Gemfile.lock).


#include "world.hpp"

using cucumber::ScenarioScope;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's remove this line and explicitly use cucumber::ScenarioScope whever it is called.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

dropped the using alias in both step files; every call site now spells out cucumber::ScenarioScope.

EXPECT_EQ(streams.size(), static_cast<std::size_t>(0));
}

WHEN("^I create a stream with name \"(.*)\"$") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's change the regex from (.*) to ([^"]{1,255}) to reflect actual stream name restrictions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also do for topic as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

tightened the name captures to ([^"]{1,255}) for both stream and topic (create + should-have-name steps).

Comment on lines +67 to +75
WHEN("^I create a topic with name \"(.*)\" in stream ([0-9]+) with ([0-9]+) partitions$") {
REGEX_PARAM(std::string, topic_name);
REGEX_PARAM(int, stream_id);
REGEX_PARAM(int, partitions_count);
ScenarioScope<bdd::GlobalContext> context;

context->client->create_topic(bdd::make_numeric_identifier(static_cast<std::uint32_t>(stream_id)), topic_name,
static_cast<std::uint32_t>(partitions_count), "none", 0, "never_expire", 0,
"server_default");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of passing strings like "server_default" and "never_expire" here, I think we can use the functions and classes in iggy.hpp

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can also be done for the rest of the functions. This will help us understand what the sdk is not providing right now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

switched create_topic to CompressionAlgorithm::none() / Expiry::never_expire() / MaxTopicSize::server_default(), and poll to PollingStrategy::offset(). the consumer kind and partitioning are still raw strings — iggy.hpp has no typed wrapper for those yet, so they mark exactly the gaps you mentioned.

THEN("^all messages should be sent successfully$") {
ScenarioScope<bdd::GlobalContext> context;

EXPECT_GT(context->sent_count, static_cast<std::uint32_t>(0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we tighten this to expect the exact message count?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

now asserts the exact count: the send step records how many messages it dispatched and this step checks sent == requested instead of just > 0.

@hubcio hubcio removed the S-waiting-on-review PR is waiting on a reviewer label Jul 1, 2026
@hubcio hubcio added the S-waiting-on-author PR is waiting on author response label Jul 1, 2026
The oversized-payload test grew its 64 MB buffer one byte at a time and a comment claimed cxx::Vec had no public reserve API. It does, so reserve the capacity once before filling it.

Signed-off-by: seokjin0414 <sars21@hanmail.net>
The C++ SDK had no BDD coverage. Add cucumber-cpp step definitions under
bdd/cpp that drive the shared basic_messaging.feature through the C++ FFI
client.

cucumber-cpp uses the wire protocol, so bdd/cpp is a self-contained Bazel
module: it builds the step definitions into a wire server (pulling
cucumber-cpp from the Bazel registry and the Iggy C++ SDK from foreign/cpp
via local_path_override) that a Ruby Cucumber runner connects to. The SDK's
iggy-cpp target is made public so the harness can depend on it. A per-file
CUKE_OBJECT_PREFIX keeps the generated step classes from clashing across
translation units. The Gemfile and cucumber.wire comment styles are
registered with the license checker.

Signed-off-by: seokjin0414 <sars21@hanmail.net>
A Ruby cucumber runner with cucumber-wire drives the wire server over the
wire protocol against a real iggy-server. Register the cpp-bdd compose
service, the run-bdd-tests entry, and the bdd-cpp CI component so it runs
like the other SDK BDD suites.

Signed-off-by: seokjin0414 <sars21@hanmail.net>
@seokjin0414 seokjin0414 force-pushed the 2965-cpp-bdd-tests branch from 61e1c76 to 78c3ba4 Compare July 2, 2026 10:21
@seokjin0414

Copy link
Copy Markdown
Contributor Author

/ready

@github-actions github-actions Bot added S-waiting-on-review PR is waiting on a reviewer and removed S-waiting-on-author PR is waiting on author response labels Jul 2, 2026
@seokjin0414 seokjin0414 requested a review from slbotbm July 2, 2026 11:05
cd /workspace/bdd/cpp || exit 1

set +e
bundle exec cucumber

@hubcio hubcio Jul 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

cucumber 7.1.0 defaults to non-strict, so an undefined step exits 0 - if the shared feature file ever gains a step without a matching C++ regex, this suite silently goes green. bundle exec cucumber --strict closes that.

fwiw rust (cucumber-rs reports an unmatched step as skipped -> exit 0) and go (godog, non-strict by default) have the same hole and would also pass silently - node/java/python/csharp runners are strict by default. so worth adding --strict here, and probably to rust/go too (separate PRs).

separately, the image bakes no .feature files and the copy on line 24 is masked with 2>/dev/null || true, so a missing mount means 0 scenarios and exit 0 even with --strict. worth a guard like ls features/*.feature >/dev/null || exit 1 before running. both fixes needed, they cover different holes.


bdd_wire_server &
server_pid=$!
sleep 2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

fixed sleep 2 instead of waiting for port 3902. fails safe (connection refused = red), so worst case is a flake on a slow machine - a small wait-for-port loop would remove that.

THEN("^all messages should be sent successfully$") {
cucumber::ScenarioScope<bdd::GlobalContext> context;

EXPECT_EQ(context->sent_count, context->messages_to_send);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this assert can never fail - sent_count and messages_to_send are both derived from the same message_count in the WHEN step. the real signal is send_messages() throwing there. rust and python versions of this step are equally weak, so fine to keep for parity, but the sent_count/messages_to_send bookkeeping could be dropped.

Comment thread bdd/cpp/features/step_definitions/world.hpp
// (rust::Vec) across steps.
struct PolledData {
std::uint32_t count = 0;
std::uint64_t current_offset = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

current_offset is write-only - set in the poll step (messaging_steps.cpp:160) but never read by any THEN. field and assignment can go.

Comment thread bdd/cpp/Gemfile.lock
ffi (~> 1.1)
memoist3 (~> 1.0.0)

PLATFORMS

@hubcio hubcio Jul 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PLATFORMS is missing x86_64-linux (the CI runner arch), so in the image ffi installs via the generic ruby platform and compiles from source. checked empirically with the pinned bundler 2.6.9: bundle install leaves the lock byte-identical (generic ruby entry already satisfies x86_64-linux, nothing gets appended), and a frozen install succeeds against the current lock as-is - so no mutation, no correctness issue. the only gain from bundle lock --add-platform x86_64-linux + a frozen install is turning future Gemfile/lock drift into a hard error instead of a silent re-resolve. minor, optional.

Comment thread bdd/cpp/.bazelrc
# specific language governing permissions and limitations
# under the License.

common --enable_bzlmod

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the committed MODULE.bazel.lock is never enforced - bazel 9 defaults to lockfile_mode=update, so it silently drifts. foreign/cpp/.bazelrc sets --lockfile_mode=error under its ci config and CI builds with --config=ci; same setup here would keep the lock honest.

@slbotbm please confirm

@slbotbm slbotbm Jul 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we can add the required config here as well then

Comment thread bdd/cpp/Dockerfile

# Stage 2: install the Ruby Cucumber runner. build-essential is needed to compile the ffi
# native extension and never reaches the runtime image.
FROM ruby:3.3-slim-trixie AS deps

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

heads up: the ruby 3.3 pin is load-bearing - cucumber 7.1.0 crashes on ruby 3.4 (unknown keywords: :strict, :proc from the Hash.new keyword change). worth a comment here so a future base-image bump doesn't trip on it.

NOTE: i didnt verify it personally

@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-author PR is waiting on author response

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants