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