You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iggy.apache.org by hg...@apache.org on 2025/06/26 15:40:08 UTC
(iggy) branch bdd-python updated (b99d74b6 -> cfc8ff97)
This is an automated email from the ASF dual-hosted git repository.
hgruszecki pushed a change to branch bdd-python
in repository https://gitbox.apache.org/repos/asf/iggy.git
omit b99d74b6 feat(bdd): add Python BDD tests and reorganize test structure
new cfc8ff97 feat(bdd): add Python BDD tests and reorganize test structure
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (b99d74b6)
\
N -- N -- N refs/heads/bdd-python (cfc8ff97)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
bdd/python/Dockerfile | 18 +++++++++---------
bdd/rust/Dockerfile | 2 +-
2 files changed, 10 insertions(+), 10 deletions(-)
(iggy) 01/01: feat(bdd): add Python BDD tests and reorganize test structure
Posted by hg...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hgruszecki pushed a commit to branch bdd-python
in repository https://gitbox.apache.org/repos/asf/iggy.git
commit cfc8ff97cf61d8a80ea8539505ed0a697ce836e3
Author: Hubert Gruszecki <h....@gmail.com>
AuthorDate: Sat Jun 7 21:53:28 2025 +0200
feat(bdd): add Python BDD tests and reorganize test structure
Introduces cross-SDK BDD testing framework with Python implementation
alongside existing Rust tests. Moves BDD tests from core/bdd to a
dedicated top-level bdd/ directory organized by language.
- Add Python BDD tests using pytest-bdd for basic messaging scenarios
- Reorganize Rust BDD tests from core/bdd/ to bdd/rust/
- Create Docker infrastructure for running multi-SDK tests
- Add run-bdd-tests.sh script for easy test execution
- Share Gherkin feature files across all SDK implementations
---
.github/changed-files-config.json | 16 +-
.../workflows/ci-check-bdd.yml | 51 +++---
.github/workflows/ci-check-pr.yml | 8 +
.github/workflows/ci-check-python-sdk.yml | 2 +-
Cargo.toml | 2 +-
core/bdd/Cargo.toml => bdd/Dockerfile | 57 +++---
bdd/README.md | 94 ++++++++++
bdd/docker-compose.yml | 69 ++++++++
bdd/python/Dockerfile | 55 ++++++
bdd/python/requirements.txt | 4 +
bdd/python/tests/conftest.py | 65 +++++++
bdd/python/tests/test_basic_messaging.py | 193 +++++++++++++++++++++
{core/bdd => bdd/rust}/Cargo.toml | 9 +-
core/bdd/Cargo.toml => bdd/rust/Dockerfile | 38 ++--
{core/bdd => bdd/rust}/tests/basic_messaging.rs | 2 +-
.../rust}/tests/common/global_context.rs | 3 +
{core/bdd => bdd/rust}/tests/common/mod.rs | 0
{core/bdd => bdd/rust}/tests/helpers/mod.rs | 0
{core/bdd => bdd/rust}/tests/helpers/test_data.rs | 0
{core/bdd => bdd/rust}/tests/steps/auth.rs | 0
{core/bdd => bdd/rust}/tests/steps/messages.rs | 0
{core/bdd => bdd/rust}/tests/steps/mod.rs | 0
{core/bdd => bdd/rust}/tests/steps/server.rs | 25 ++-
{core/bdd => bdd/rust}/tests/steps/streams.rs | 0
{core/bdd => bdd/rust}/tests/steps/topics.rs | 0
bdd/{ => scenarios}/basic_messaging.feature | 6 +-
scripts/run-bdd-tests.sh | 64 +++++++
27 files changed, 677 insertions(+), 86 deletions(-)
diff --git a/.github/changed-files-config.json b/.github/changed-files-config.json
index 5aa6ea9b..c01409ee 100644
--- a/.github/changed-files-config.json
+++ b/.github/changed-files-config.json
@@ -1,6 +1,7 @@
{
"rust": [
"core/(bench|cli|examples|integration|sdk|server|tools|connectors).*\\.rs",
+ "bdd/rust/.*\\.rs",
"core/Cargo.toml",
"Cargo.toml",
"Cargo.lock",
@@ -15,6 +16,7 @@
],
"rust-prod": [
"core/(bench|cli|examples|integration|sdk|server|tools|connectors).*\\.rs",
+ "bdd/rust/.*\\.rs",
"core/Cargo.toml",
"Cargo.toml",
"Cargo.lock",
@@ -30,6 +32,7 @@
"python-sdk": [
"foreign/python/.*\\.py",
"foreign/python/.*\\.rs",
+ "bdd/python/.*\\.py",
"foreign/python/pyproject.toml",
"foreign/python/Cargo.toml",
"foreign/python/docker-compose.test.yml",
@@ -44,11 +47,20 @@
"foreign/node/.*\\.js",
"foreign/node/.*\\.json",
".github/workflows/ci-check-node-sdk.yml"
- ]
- ,
+ ],
"csharp-sdk": [
"foreign/csharp/.*\\.cs",
"foreign/csharp/.*\\.csproj",
".github/workflows/ci-check-csharp-sdk.yml"
+ ],
+ "bdd": [
+ "bdd/.*\\.feature",
+ "bdd/.*\\.py",
+ "bdd/.*\\.rs",
+ "bdd/.*Dockerfile",
+ "bdd/docker-compose.yml",
+ "scripts/run-bdd-tests.sh",
+ ".github/workflows/ci-check-bdd.yml",
+ "core/.*\\.rs"
]
}
diff --git a/core/bdd/Cargo.toml b/.github/workflows/ci-check-bdd.yml
similarity index 52%
copy from core/bdd/Cargo.toml
copy to .github/workflows/ci-check-bdd.yml
index c459f2dd..a72dff83 100644
--- a/core/bdd/Cargo.toml
+++ b/.github/workflows/ci-check-bdd.yml
@@ -15,29 +15,34 @@
# specific language governing permissions and limitations
# under the License.
-[package]
-description = "Behavior-Driven Development (BDD) tests for Apache Iggy"
-name = "bdd"
-version = "0.0.1"
-edition = "2024"
-license = "Apache-2.0"
+name: Cross-SDK BDD Tests
-[dev-dependencies]
-bytes = { workspace = true }
-cucumber = "0.21"
-iggy = { workspace = true }
-integration = { path = "../integration" }
-tokio = { workspace = true }
+on:
+ workflow_call:
-[[test]]
-name = "basic_messaging"
-harness = false
+jobs:
+ bdd-tests:
+ name: BDD tests (${{ matrix.sdk }})
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ sdk: [rust, python]
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Cache Rust dependencies
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: |
+ . -> target
+ foreign/python -> target
+ cache-on-failure: true
+ key: bdd-${{ matrix.sdk }}
-# Future test scenarios can be added here:
-# [[test]]
-# name = "user_management"
-# harness = false
-#
-# [[test]]
-# name = "consumer_groups"
-# harness = false
+ - name: Run BDD tests for ${{ matrix.sdk }}
+ run: ./scripts/run-bdd-tests.sh ${{ matrix.sdk }}
+
+ - name: Clean up Docker resources
+ if: always()
+ run: ./scripts/run-bdd-tests.sh clean
\ No newline at end of file
diff --git a/.github/workflows/ci-check-pr.yml b/.github/workflows/ci-check-pr.yml
index 5f2f6ca3..033b8de1 100644
--- a/.github/workflows/ci-check-pr.yml
+++ b/.github/workflows/ci-check-pr.yml
@@ -63,6 +63,7 @@ jobs:
trigger-python-sdk: ${{ steps.changed-files.outputs.PYTHON-SDK_FILES_CHANGED }}
trigger-node-sdk: ${{ steps.changed-files.outputs.NODE-SDK_FILES_CHANGED }}
trigger-csharp-sdk: ${{ steps.changed-files.outputs.CSHARP-SDK_FILES_CHANGED }}
+ trigger-bdd: ${{ steps.changed-files.outputs.BDD_FILES_CHANGED }}
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -151,6 +152,12 @@ jobs:
if: ${{ needs.pr-file-changes.outputs.trigger-csharp-sdk == 'true' }}
uses: ./.github/workflows/ci-check-csharp-sdk.yml
+ ci-check-bdd:
+ name: ci-check-bdd
+ needs: pr-file-changes
+ if: ${{ needs.pr-file-changes.outputs.trigger-bdd == 'true' }}
+ uses: ./.github/workflows/ci-check-bdd.yml
+
finalize_pr:
runs-on: ubuntu-latest
needs:
@@ -164,6 +171,7 @@ jobs:
- ci-check-python-sdk
- ci-check-node-sdk
- ci-check-csharp-sdk
+ - ci-check-bdd
if: always()
steps:
- name: Everything is fine
diff --git a/.github/workflows/ci-check-python-sdk.yml b/.github/workflows/ci-check-python-sdk.yml
index 49b7d38d..2f93c010 100644
--- a/.github/workflows/ci-check-python-sdk.yml
+++ b/.github/workflows/ci-check-python-sdk.yml
@@ -71,7 +71,7 @@ jobs:
run: |
# Run tests using Docker Compose with caching
DOCKER_BUILDKIT=1 docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from python-tests
-
+
# Clean up
docker compose -f docker-compose.test.yml down -v
- name: Upload wheels
diff --git a/Cargo.toml b/Cargo.toml
index 479e12c3..1fa553c3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,7 +21,7 @@ codegen-units = 1
[workspace]
members = [
- "core/bdd",
+ "bdd/rust",
"core/bench",
"core/bench/dashboard/frontend",
"core/bench/dashboard/server",
diff --git a/core/bdd/Cargo.toml b/bdd/Dockerfile
similarity index 55%
copy from core/bdd/Cargo.toml
copy to bdd/Dockerfile
index c459f2dd..736358b6 100644
--- a/core/bdd/Cargo.toml
+++ b/bdd/Dockerfile
@@ -15,29 +15,34 @@
# specific language governing permissions and limitations
# under the License.
-[package]
-description = "Behavior-Driven Development (BDD) tests for Apache Iggy"
-name = "bdd"
-version = "0.0.1"
-edition = "2024"
-license = "Apache-2.0"
-
-[dev-dependencies]
-bytes = { workspace = true }
-cucumber = "0.21"
-iggy = { workspace = true }
-integration = { path = "../integration" }
-tokio = { workspace = true }
-
-[[test]]
-name = "basic_messaging"
-harness = false
-
-# Future test scenarios can be added here:
-# [[test]]
-# name = "user_management"
-# harness = false
-#
-# [[test]]
-# name = "consumer_groups"
-# harness = false
+FROM rust:1.87 as builder
+
+WORKDIR /app
+COPY . .
+
+# Build server in debug mode for faster compilation
+RUN cargo build --bin iggy-server
+
+FROM debian:bookworm-slim
+
+# Install curl for healthcheck
+RUN apt-get update && \
+ apt-get install -y curl && \
+ rm -rf /var/lib/apt/lists/*
+
+# Copy server binary
+COPY --from=builder /app/target/debug/iggy-server /usr/local/bin/
+
+# Copy configs directory
+COPY core/configs ./configs
+
+# Create directory for data
+RUN mkdir -p /app/local_data
+
+WORKDIR /app
+
+# Expose ports
+EXPOSE 8090 3000 8080
+
+# Start server with --fresh flag to clean local data on startup
+CMD ["iggy-server", "--fresh"]
diff --git a/bdd/README.md b/bdd/README.md
new file mode 100644
index 00000000..5c364243
--- /dev/null
+++ b/bdd/README.md
@@ -0,0 +1,94 @@
+# Cross-SDK BDD Testing
+
+This directory contains cross-SDK Behavior-Driven Development (BDD) tests for Apache Iggy, designed to ensure consistency across different language SDKs.
+
+## Structure
+
+```bash
+bdd/
+├── scenarios/ # Shared Gherkin feature files
+│ └── basic_messaging.feature
+├── rust/ # Rust SDK BDD implementation
+│ ├── Dockerfile # Rust BDD test container
+│ ├── tests/
+│ └── Cargo.toml
+├── python/ # Python SDK BDD implementation
+│ ├── Dockerfile # Python BDD test container
+│ ├── tests/
+│ └── requirements.txt
+├── docker-compose.yml # Orchestrates server + SDK containers
+├── Dockerfile # Debug build of Iggy server
+└── README.md
+```
+
+## Usage
+
+### Quick Start
+
+```bash
+# Run all SDK tests
+../scripts/run-bdd-tests.sh all
+
+# Run specific SDK tests
+../scripts/run-bdd-tests.sh rust
+../scripts/run-bdd-tests.sh python
+
+# Clean up Docker resources
+../scripts/run-bdd-tests.sh clean
+```
+
+### Requirements
+
+- Docker and Docker Compose
+- The tests build the latest Iggy server from source in debug mode for faster compilation
+
+### How it Works
+
+1. **Server Container**: Builds and runs the latest Iggy server in debug mode
+2. **SDK Containers**: Each SDK has its own container with the appropriate runtime and dependencies
+3. **Shared Features**: All SDKs test against the same `.feature` files for consistency
+4. **Health Checks**: Containers wait for the server to be healthy before running tests
+
+### Adding New SDKs
+
+To add a new SDK (e.g., Node.js):
+
+1. Create `node/` directory
+2. Add `node/Dockerfile` with appropriate runtime and dependencies
+3. Create `node/tests/` directory with BDD implementation
+4. Add `node-bdd` service to `docker-compose.yml`
+5. Update `../scripts/run-bdd-tests.sh` script
+
+### CI/CD Integration
+
+Example GitHub Actions workflow:
+
+```yaml
+name: Cross-SDK BDD Tests
+on: [pull_request]
+
+jobs:
+ bdd-tests:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ sdk: [rust, python]
+ steps:
+ - uses: actions/checkout@v3
+ - name: Run BDD tests for ${{ matrix.sdk }}
+ run: ./scripts/run-bdd-tests.sh ${{ matrix.sdk }}
+```
+
+## Development
+
+### For Rust SDK
+
+The Rust implementation is located in `core/bdd/` and linked via Docker volumes.
+
+### For Python SDK
+
+The Python implementation is in `bdd/python/tests/` and needs to be updated as the Python SDK API evolves.
+
+### Adding New Scenarios
+
+Add new `.feature` files to the `bdd/scenarios/` directory and implement the corresponding step definitions in each SDK's test directory.
diff --git a/bdd/docker-compose.yml b/bdd/docker-compose.yml
new file mode 100644
index 00000000..0c262dbd
--- /dev/null
+++ b/bdd/docker-compose.yml
@@ -0,0 +1,69 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+version: '3.8'
+
+services:
+ iggy-server:
+ build:
+ context: ..
+ dockerfile: bdd/Dockerfile
+ ports:
+ - "8090:8090" # TCP
+ - "3000:3000" # HTTP
+ - "8080:8080" # QUIC
+ environment:
+ - RUST_LOG=info
+ - IGGY_SYSTEM_PATH=local_data
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:3000/stats"]
+ interval: 5s
+ timeout: 10s
+ retries: 10
+ start_period: 30s
+ volumes:
+ - iggy_data:/app/local_data
+
+ rust-bdd:
+ build:
+ context: ..
+ dockerfile: bdd/rust/Dockerfile
+ depends_on:
+ iggy-server:
+ condition: service_healthy
+ environment:
+ - IGGY_TCP_ADDRESS=iggy-server:8090
+ volumes:
+ - ./scenarios/basic_messaging.feature:/app/features/basic_messaging.feature
+ command: ["cargo", "test", "-p", "bdd", "--features", "iggy-server-in-docker"]
+
+ python-bdd:
+ build:
+ context: ..
+ dockerfile: bdd/python/Dockerfile
+ depends_on:
+ iggy-server:
+ condition: service_healthy
+ environment:
+ - IGGY_TCP_ADDRESS=iggy-server:8090
+ volumes:
+ - ./scenarios/basic_messaging.feature:/app/features/basic_messaging.feature
+ working_dir: /app
+ command: ["pytest", "tests/", "-v"]
+
+volumes:
+ iggy_data:
\ No newline at end of file
diff --git a/bdd/python/Dockerfile b/bdd/python/Dockerfile
new file mode 100644
index 00000000..939f9fc5
--- /dev/null
+++ b/bdd/python/Dockerfile
@@ -0,0 +1,55 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+FROM rust:1.87-slim-bookworm
+
+RUN apt-get update && \
+ apt-get install -y \
+ python3.11 \
+ python3.11-dev \
+ python3-pip \
+ build-essential \
+ && rm -rf /var/lib/apt/lists/*
+
+# Configure pip to allow system-wide installations in Docker
+ENV PIP_BREAK_SYSTEM_PACKAGES=1
+
+# Copy the entire workspace for dependencies
+WORKDIR /workspace
+COPY . .
+
+# Set working directory for Python SDK
+WORKDIR /workspace/foreign/python
+
+# Install Python dependencies including BDD requirements
+RUN pip3 install --no-cache-dir -r /workspace/bdd/python/requirements.txt
+
+# Build and install the Iggy Python SDK
+RUN maturin build --release --out dist && \
+ pip3 install dist/*.whl
+
+# Set up BDD test directory
+WORKDIR /app
+RUN mkdir -p /app/features
+RUN mkdir -p /app/tests
+
+# Copy BDD test files
+RUN cp -r /workspace/bdd/python/tests/* /app/tests/
+RUN cp /workspace/bdd/python/pytest.ini /app/
+
+# Default command
+CMD ["pytest", "tests/", "-v"]
diff --git a/bdd/python/requirements.txt b/bdd/python/requirements.txt
new file mode 100644
index 00000000..2710db4a
--- /dev/null
+++ b/bdd/python/requirements.txt
@@ -0,0 +1,4 @@
+pytest>=7.0.0
+pytest-bdd>=6.0.0
+pytest-asyncio>=0.21.0
+maturin>=1.0.0
\ No newline at end of file
diff --git a/bdd/python/tests/conftest.py b/bdd/python/tests/conftest.py
new file mode 100644
index 00000000..5a71149a
--- /dev/null
+++ b/bdd/python/tests/conftest.py
@@ -0,0 +1,65 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+BDD test configuration and fixtures for Python SDK tests.
+"""
+import asyncio
+import os
+import pytest
+from dataclasses import dataclass
+from typing import Optional, List
+
+
+@dataclass
+class GlobalContext:
+ """Global test context similar to Rust implementation."""
+ client: Optional[object] = None # Will be IggyClient
+ server_addr: Optional[str] = None
+ last_stream_id: Optional[int] = None
+ last_stream_name: Optional[str] = None
+ last_topic_id: Optional[int] = None
+ last_topic_name: Optional[str] = None
+ last_topic_partitions: Optional[int] = None
+ last_polled_messages: Optional[List[object]] = None # Will be List[ReceiveMessage]
+ last_sent_message: Optional[str] = None # Store message payload as string
+
+
+@pytest.fixture(scope="session")
+def event_loop():
+ """Create an instance of the default event loop for the test session."""
+ loop = asyncio.get_event_loop_policy().new_event_loop()
+ yield loop
+ loop.close()
+
+
+@pytest.fixture(scope="function")
+async def context():
+ """Create a fresh context for each test scenario."""
+ ctx = GlobalContext()
+
+ # Get server address from environment or use default
+ ctx.server_addr = os.environ.get("IGGY_TCP_ADDRESS", "127.0.0.1:8090")
+
+ yield ctx
+
+ # Cleanup: disconnect client if connected
+ if ctx.client:
+ try:
+ await ctx.client.disconnect()
+ except:
+ pass # Ignore cleanup errors
\ No newline at end of file
diff --git a/bdd/python/tests/test_basic_messaging.py b/bdd/python/tests/test_basic_messaging.py
new file mode 100644
index 00000000..9847dc9a
--- /dev/null
+++ b/bdd/python/tests/test_basic_messaging.py
@@ -0,0 +1,193 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Basic messaging BDD test implementation for Python SDK
+"""
+import asyncio
+from pytest_bdd import scenarios, given, when, then, parsers
+from iggy_py import IggyClient, SendMessage, PollingStrategy
+
+# Load scenarios from the shared feature file
+scenarios('../../scenarios/basic_messaging.feature')
+
+
+@given('I have a running Iggy server')
+async def running_server(context):
+ """Ensure we have a running Iggy server and create client"""
+ context.client = IggyClient(context.server_addr)
+ await context.client.connect()
+ await context.client.ping() # Health check
+
+
+@given('I am authenticated as the root user')
+async def authenticated_root_user(context):
+ """Authenticate as root user"""
+ await context.client.login_user("iggy", "iggy")
+
+
+@given('I have no streams in the system')
+async def no_streams_in_system(context):
+ """Ensure no streams exist in the system"""
+ # With --fresh flag on server, this should already be clean
+ # Just verify by attempting to get a stream that shouldn't exist
+ pass
+
+
+@when(parsers.parse('I create a stream with ID {stream_id:d} and name {stream_name}'))
+async def create_stream(context, stream_id, stream_name):
+ """Create a stream with specified ID and name"""
+ await context.client.create_stream(name=stream_name, stream_id=stream_id)
+ context.last_stream_id = stream_id
+ context.last_stream_name = stream_name
+
+
+@then('the stream should be created successfully')
+async def stream_created_successfully(context):
+ """Verify stream was created successfully"""
+ stream = await context.client.get_stream(context.last_stream_name)
+ assert stream is not None
+
+
+@then(parsers.parse('the stream should have ID {stream_id:d} and name {stream_name}'))
+async def verify_stream_properties(context, stream_id, stream_name):
+ """Verify stream has correct ID and name"""
+ stream = await context.client.get_stream(stream_name)
+ assert stream is not None
+ assert stream.id == stream_id
+ assert stream.name == stream_name
+
+
+@when(parsers.parse('I create a topic with ID {topic_id:d} and name {topic_name} in stream {stream_id:d} with {partitions:d} partitions'))
+async def create_topic(context, topic_id, topic_name, stream_id, partitions):
+ """Create a topic with specified parameters"""
+ await context.client.create_topic(
+ stream=stream_id,
+ name=topic_name,
+ partitions_count=partitions,
+ topic_id=topic_id
+ )
+ context.last_topic_id = topic_id
+ context.last_topic_name = topic_name
+ context.last_topic_partitions = partitions
+
+
+@then('the topic should be created successfully')
+async def topic_created_successfully(context):
+ """Verify topic was created successfully"""
+ topic = await context.client.get_topic(context.last_stream_id, context.last_topic_name)
+ assert topic is not None
+
+
+@then(parsers.parse('the topic should have ID {topic_id:d} and name {topic_name}'))
+async def verify_topic_properties(context, topic_id, topic_name):
+ """Verify topic has correct ID and name"""
+ topic = await context.client.get_topic(context.last_stream_id, topic_name)
+ assert topic is not None
+ assert topic.id == topic_id
+ assert topic.name == topic_name
+
+
+@then(parsers.parse('the topic should have {partitions:d} partitions'))
+async def verify_topic_partitions(context, partitions):
+ """Verify topic has correct number of partitions"""
+ topic = await context.client.get_topic(context.last_stream_id, context.last_topic_name)
+ assert topic is not None
+ assert topic.partitions_count == partitions
+
+
+@when(parsers.parse('I send {message_count:d} messages to stream {stream_id:d}, topic {topic_id:d}, partition {partition_id:d}'))
+async def send_messages(context, message_count, stream_id, topic_id, partition_id):
+ """Send messages to specified stream, topic, and partition"""
+ messages = []
+ for i in range(message_count):
+ content = f"test message {i}"
+ messages.append(SendMessage(content))
+
+ await context.client.send_messages(
+ stream=stream_id,
+ topic=topic_id,
+ partitioning=partition_id,
+ messages=messages
+ )
+
+ # Store the last sent message content for comparison
+ if messages:
+ context.last_sent_message = f"test message {message_count - 1}"
+
+
+@then('all messages should be sent successfully')
+async def messages_sent_successfully(context):
+ """Verify all messages were sent successfully"""
+ # If we got here without exception, messages were sent successfully
+ assert context.last_sent_message is not None
+
+
+@when(parsers.parse('I poll messages from stream {stream_id:d}, topic {topic_id:d}, partition {partition_id:d} starting from offset {start_offset:d}'))
+async def poll_messages(context, stream_id, topic_id, partition_id, start_offset):
+ """Poll messages from specified location"""
+ context.last_polled_messages = await context.client.poll_messages(
+ stream=stream_id,
+ topic=topic_id,
+ partition_id=partition_id,
+ polling_strategy=PollingStrategy.Offset(value=start_offset),
+ count=100, # Poll up to 100 messages
+ auto_commit=True
+ )
+
+
+@then(parsers.parse('I should receive {expected_count:d} messages'))
+def verify_message_count(context, expected_count):
+ """Verify correct number of messages received"""
+ assert context.last_polled_messages is not None
+ assert len(context.last_polled_messages) == expected_count
+
+
+@then(parsers.parse('the messages should have sequential offsets from {start_offset:d} to {end_offset:d}'))
+def verify_sequential_offsets(context, start_offset, end_offset):
+ """Verify messages have sequential offsets"""
+ assert context.last_polled_messages is not None
+
+ for i, message in enumerate(context.last_polled_messages):
+ expected_offset = start_offset + i
+ assert message.offset() == expected_offset
+
+ last_message = context.last_polled_messages[-1]
+ assert last_message.offset() == end_offset
+
+
+@then('each message should have the expected payload content')
+def verify_payload_content(context):
+ """Verify each message has expected payload content"""
+ assert context.last_polled_messages is not None
+
+ for i, message in enumerate(context.last_polled_messages):
+ expected_payload = f"test message {i}"
+ actual_payload = message.payload().decode('utf-8')
+ assert actual_payload == expected_payload
+
+
+@then('the last polled message should match the last sent message')
+def verify_last_message_match(context):
+ """Verify last polled message matches last sent message"""
+ assert context.last_sent_message is not None
+ assert context.last_polled_messages is not None
+
+ last_polled = context.last_polled_messages[-1]
+ last_polled_payload = last_polled.payload().decode('utf-8')
+
+ assert last_polled_payload == context.last_sent_message
\ No newline at end of file
diff --git a/core/bdd/Cargo.toml b/bdd/rust/Cargo.toml
similarity index 80%
copy from core/bdd/Cargo.toml
copy to bdd/rust/Cargo.toml
index c459f2dd..178615bc 100644
--- a/core/bdd/Cargo.toml
+++ b/bdd/rust/Cargo.toml
@@ -22,16 +22,23 @@ version = "0.0.1"
edition = "2024"
license = "Apache-2.0"
+[features]
+iggy-server-in-docker = []
+
[dev-dependencies]
bytes = { workspace = true }
cucumber = "0.21"
iggy = { workspace = true }
-integration = { path = "../integration" }
+integration = { workspace = true }
tokio = { workspace = true }
+# `test` is set to false because cucumber uses its own test harness, which
+# prevents running specific tests with commands like `cargo test some_test`.
+# Besides that, those tests are executed in the CI pipeline.
[[test]]
name = "basic_messaging"
harness = false
+test = false
# Future test scenarios can be added here:
# [[test]]
diff --git a/core/bdd/Cargo.toml b/bdd/rust/Dockerfile
similarity index 60%
rename from core/bdd/Cargo.toml
rename to bdd/rust/Dockerfile
index c459f2dd..e7b36dbf 100644
--- a/core/bdd/Cargo.toml
+++ b/bdd/rust/Dockerfile
@@ -15,29 +15,21 @@
# specific language governing permissions and limitations
# under the License.
-[package]
-description = "Behavior-Driven Development (BDD) tests for Apache Iggy"
-name = "bdd"
-version = "0.0.1"
-edition = "2024"
-license = "Apache-2.0"
+FROM rust:1.87
-[dev-dependencies]
-bytes = { workspace = true }
-cucumber = "0.21"
-iggy = { workspace = true }
-integration = { path = "../integration" }
-tokio = { workspace = true }
+WORKDIR /app
-[[test]]
-name = "basic_messaging"
-harness = false
+# Copy entire repository (needed for workspace)
+COPY .. .
-# Future test scenarios can be added here:
-# [[test]]
-# name = "user_management"
-# harness = false
-#
-# [[test]]
-# name = "consumer_groups"
-# harness = false
+# Create features directory for BDD feature files
+RUN mkdir -p /app/features
+
+# Build dependencies for workspace
+RUN cargo fetch
+
+# Build the BDD project
+RUN cargo build -p bdd
+
+# Default command will be overridden by docker-compose
+CMD ["cargo", "test", "-p", "bdd", "--features", "iggy-server-in-docker"]
diff --git a/core/bdd/tests/basic_messaging.rs b/bdd/rust/tests/basic_messaging.rs
similarity index 92%
rename from core/bdd/tests/basic_messaging.rs
rename to bdd/rust/tests/basic_messaging.rs
index b2f34976..01fd4982 100644
--- a/core/bdd/tests/basic_messaging.rs
+++ b/bdd/rust/tests/basic_messaging.rs
@@ -25,5 +25,5 @@ use cucumber::World;
#[tokio::main]
async fn main() {
- GlobalContext::run("../../bdd/basic_messaging.feature").await;
+ GlobalContext::run("../../bdd/scenarios/basic_messaging.feature").await;
}
diff --git a/core/bdd/tests/common/global_context.rs b/bdd/rust/tests/common/global_context.rs
similarity index 93%
rename from core/bdd/tests/common/global_context.rs
rename to bdd/rust/tests/common/global_context.rs
index be6a954c..a526f873 100644
--- a/core/bdd/tests/common/global_context.rs
+++ b/bdd/rust/tests/common/global_context.rs
@@ -19,10 +19,13 @@
use cucumber::World;
use iggy::clients::client::IggyClient;
use iggy::prelude::{IggyMessage, PolledMessages};
+
+#[cfg(not(feature = "iggy-server-in-docker"))]
use integration::test_server::TestServer;
#[derive(Debug, World, Default)]
pub struct GlobalContext {
+ #[cfg(not(feature = "iggy-server-in-docker"))]
pub server: Option<TestServer>,
pub client: Option<IggyClient>,
pub server_addr: Option<String>,
diff --git a/core/bdd/tests/common/mod.rs b/bdd/rust/tests/common/mod.rs
similarity index 100%
rename from core/bdd/tests/common/mod.rs
rename to bdd/rust/tests/common/mod.rs
diff --git a/core/bdd/tests/helpers/mod.rs b/bdd/rust/tests/helpers/mod.rs
similarity index 100%
rename from core/bdd/tests/helpers/mod.rs
rename to bdd/rust/tests/helpers/mod.rs
diff --git a/core/bdd/tests/helpers/test_data.rs b/bdd/rust/tests/helpers/test_data.rs
similarity index 100%
rename from core/bdd/tests/helpers/test_data.rs
rename to bdd/rust/tests/helpers/test_data.rs
diff --git a/core/bdd/tests/steps/auth.rs b/bdd/rust/tests/steps/auth.rs
similarity index 100%
rename from core/bdd/tests/steps/auth.rs
rename to bdd/rust/tests/steps/auth.rs
diff --git a/core/bdd/tests/steps/messages.rs b/bdd/rust/tests/steps/messages.rs
similarity index 100%
rename from core/bdd/tests/steps/messages.rs
rename to bdd/rust/tests/steps/messages.rs
diff --git a/core/bdd/tests/steps/mod.rs b/bdd/rust/tests/steps/mod.rs
similarity index 100%
rename from core/bdd/tests/steps/mod.rs
rename to bdd/rust/tests/steps/mod.rs
diff --git a/core/bdd/tests/steps/server.rs b/bdd/rust/tests/steps/server.rs
similarity index 57%
rename from core/bdd/tests/steps/server.rs
rename to bdd/rust/tests/steps/server.rs
index e2dca32b..ad688f73 100644
--- a/core/bdd/tests/steps/server.rs
+++ b/bdd/rust/tests/steps/server.rs
@@ -18,14 +18,29 @@
use crate::common::global_context::GlobalContext;
use cucumber::given;
+
+#[cfg(not(feature = "iggy-server-in-docker"))]
use integration::test_server::TestServer;
#[given("I have a running Iggy server")]
pub async fn given_running_server(world: &mut GlobalContext) {
- let mut test_server = TestServer::default();
- test_server.start();
- let server_addr = test_server.get_raw_tcp_addr().unwrap();
+ #[cfg(feature = "iggy-server-in-docker")]
+ {
+ // External server mode - connect to server from environment
+ let server_addr =
+ std::env::var("IGGY_TCP_ADDRESS").unwrap_or_else(|_| "localhost:8090".to_string());
+ world.server_addr = Some(server_addr);
+ // No TestServer instance in external mode
+ }
+
+ #[cfg(not(feature = "iggy-server-in-docker"))]
+ {
+ // Embedded server mode - start our own TestServer
+ let mut test_server = TestServer::default();
+ test_server.start();
+ let server_addr = test_server.get_raw_tcp_addr().unwrap();
- world.server_addr = Some(server_addr);
- world.server = Some(test_server);
+ world.server_addr = Some(server_addr);
+ world.server = Some(test_server);
+ }
}
diff --git a/core/bdd/tests/steps/streams.rs b/bdd/rust/tests/steps/streams.rs
similarity index 100%
rename from core/bdd/tests/steps/streams.rs
rename to bdd/rust/tests/steps/streams.rs
diff --git a/core/bdd/tests/steps/topics.rs b/bdd/rust/tests/steps/topics.rs
similarity index 100%
rename from core/bdd/tests/steps/topics.rs
rename to bdd/rust/tests/steps/topics.rs
diff --git a/bdd/basic_messaging.feature b/bdd/scenarios/basic_messaging.feature
similarity index 99%
rename from bdd/basic_messaging.feature
rename to bdd/scenarios/basic_messaging.feature
index 1e5cda3d..1709626e 100644
--- a/bdd/basic_messaging.feature
+++ b/bdd/scenarios/basic_messaging.feature
@@ -29,15 +29,15 @@ Feature: Basic Messaging Operations
When I create a stream with ID 1 and name "test-stream"
Then the stream should be created successfully
And the stream should have ID 1 and name "test-stream"
-
+
When I create a topic with ID 1 and name "test-topic" in stream 1 with 3 partitions
Then the topic should be created successfully
And the topic should have ID 1 and name "test-topic"
And the topic should have 3 partitions
-
+
When I send 10 messages to stream 1, topic 1, partition 1
Then all messages should be sent successfully
-
+
When I poll messages from stream 1, topic 1, partition 1 starting from offset 0
Then I should receive 10 messages
And the messages should have sequential offsets from 0 to 9
diff --git a/scripts/run-bdd-tests.sh b/scripts/run-bdd-tests.sh
new file mode 100755
index 00000000..45ce63fa
--- /dev/null
+++ b/scripts/run-bdd-tests.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+set -e
+
+SDK=${1:-"all"}
+FEATURE=${2:-"scenarios/basic_messaging.feature"}
+
+echo "🧪 Running BDD tests for SDK: $SDK"
+echo "📁 Feature file: $FEATURE"
+
+# Change to BDD directory
+cd "$(dirname "$0")/../bdd"
+
+case $SDK in
+ "rust")
+ echo "🦀 Running Rust BDD tests..."
+ docker compose up --build --abort-on-container-exit rust-bdd
+ ;;
+ "python")
+ echo "🐍 Running Python BDD tests..."
+ docker compose up --build --abort-on-container-exit python-bdd
+ ;;
+ "all")
+ echo "🚀 Running all SDK BDD tests..."
+ echo "🦀 Starting with Rust tests..."
+ docker compose up --build --abort-on-container-exit rust-bdd
+ echo "🐍 Now running Python tests..."
+ docker compose up --build --abort-on-container-exit python-bdd
+ ;;
+ "clean")
+ echo "🧹 Cleaning up Docker resources..."
+ docker compose down -v
+ docker compose rm -f
+ ;;
+ *)
+ echo "❌ Unknown SDK: $SDK"
+ echo "📖 Usage: $0 [rust|python|all|clean] [feature_file]"
+ echo "📖 Examples:"
+ echo " $0 rust # Run Rust tests only"
+ echo " $0 python # Run Python tests only"
+ echo " $0 all # Run all SDK tests"
+ echo " $0 clean # Clean up Docker resources"
+ exit 1
+ ;;
+esac
+
+echo "✅ BDD tests completed for: $SDK"
\ No newline at end of file