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 14:58:34 UTC
(iggy) 01/01: feat(bdd): add Python BDD tests and reorganize test structure
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 dcd4c784b688278f846c6d83abc3d89a43f1bfda
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 | 3 +
.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 | 53 ++++++
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 | 5 +-
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 +++++++
25 files changed, 624 insertions(+), 61 deletions(-)
diff --git a/.github/changed-files-config.json b/.github/changed-files-config.json
index 5aa6ea9b..000e2594 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",
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..54681517
--- /dev/null
+++ b/bdd/python/Dockerfile
@@ -0,0 +1,53 @@
+# 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/*
+
+RUN ln -s /usr/bin/python3.11 /usr/bin/python && \
+ ln -s /usr/bin/pip3 /usr/bin/pip
+
+# Set working directory for Python SDK
+WORKDIR /iggy-python
+
+# Copy Python SDK source
+COPY . .
+
+# Install Python dependencies including BDD requirements
+COPY ../../bdd/python/requirements.txt /tmp/requirements.txt
+RUN pip install --no-cache-dir -r /tmp/requirements.txt
+
+# Build and install the Iggy Python SDK (debug mode for faster compilation)
+RUN maturin develop
+
+# Set up BDD test directory
+WORKDIR /app
+RUN mkdir -p /app/features
+RUN mkdir -p /app/tests
+
+# Copy BDD test files
+COPY ../../bdd/python/tests/ /app/tests/
+
+# Default command
+CMD ["pytest", "tests/", "-v"]
\ No newline at end of file
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 94%
copy from core/bdd/Cargo.toml
copy to bdd/rust/Cargo.toml
index c459f2dd..c5bab3c4 100644
--- a/core/bdd/Cargo.toml
+++ b/bdd/rust/Cargo.toml
@@ -22,11 +22,14 @@ 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]]
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..d854b987 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"]
\ No newline at end of file
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