Skip to content

Commit 2f34093

Browse files
committed
feat(test): add Python integration test framework and CI
Introduce a modular Python integration test framework under tests/integration/ with separated concerns (utils, workspace, config, process, instance facade). Add lifecycle, config, and client smoke tests. Include a dedicated Makefile for the test suite and a standalone GitHub Actions workflow. Made-with: Cursor
1 parent 0502a7c commit 2f34093

17 files changed

Lines changed: 820 additions & 1 deletion
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
name: Integration Tests
14+
15+
on:
16+
push:
17+
branches: ["main", "master"]
18+
pull_request:
19+
branches: ["main", "master"]
20+
workflow_dispatch:
21+
22+
env:
23+
CARGO_TERM_COLOR: always
24+
25+
jobs:
26+
integration-test:
27+
name: Integration Tests
28+
runs-on: ubuntu-latest
29+
timeout-minutes: 30
30+
31+
steps:
32+
- name: Checkout Source
33+
uses: actions/checkout@v4
34+
35+
- name: Setup Python
36+
uses: actions/setup-python@v5
37+
with:
38+
python-version: "3.11"
39+
cache: "pip"
40+
41+
- name: Setup Rust
42+
uses: dtolnay/rust-toolchain@stable
43+
44+
- name: Install System Dependencies
45+
run: |
46+
sudo apt-get update
47+
sudo apt-get install -y --no-install-recommends \
48+
cmake \
49+
libssl-dev \
50+
libcurl4-openssl-dev \
51+
pkg-config \
52+
libsasl2-dev \
53+
protobuf-compiler
54+
55+
- name: Cache Cargo
56+
uses: Swatinem/rust-cache@v2
57+
58+
- name: Build Release Binary
59+
run: cargo build --release --no-default-features --features incremental-cache
60+
61+
- name: Run Integration Tests
62+
run: make integration-test
63+
64+
- name: Upload Test Logs
65+
if: failure()
66+
uses: actions/upload-artifact@v4
67+
with:
68+
name: integration-test-logs
69+
path: tests/integration/target/
70+
retention-days: 7

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,7 @@ python/**/target/
3333
**/**.egg-info
3434
.cache/
3535
/.cursor/worktrees.json
36+
37+
# Integration test output
38+
tests/integration/target/
39+
tests/integration/.venv/

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ C_0 := \033[0m
4242
log = @printf "$(C_B)[-]$(C_0) %-15s %s\n" "$(1)" "$(2)"
4343
success = @printf "$(C_G)[✔]$(C_0) %s\n" "$(1)"
4444

45-
.PHONY: all help build build-lite dist dist-lite clean test env env-clean go-sdk-env go-sdk-build go-sdk-clean docker docker-run docker-push .check-env .build-wasm
45+
.PHONY: all help build build-lite dist dist-lite clean test env env-clean go-sdk-env go-sdk-build go-sdk-clean docker docker-run docker-push .check-env .build-wasm integration-test
4646

4747
all: build
4848

@@ -63,6 +63,8 @@ help:
6363
@echo " docker-run Run container (port 8080, mount logs)"
6464
@echo " docker-push Push image to registry"
6565
@echo ""
66+
@echo " integration-test Run integration tests (delegates to tests/integration)"
67+
@echo ""
6668
@echo " Version: $(VERSION) | Arch: $(ARCH) | OS: $(OS)"
6769

6870
build: .check-env .build-wasm
@@ -145,6 +147,7 @@ clean:
145147
@cargo clean
146148
@rm -rf $(DIST_ROOT) data logs
147149
@./scripts/clean.sh 2>/dev/null || true
150+
@$(MAKE) -C tests/integration clean 2>/dev/null || true
148151
$(call success,Done)
149152

150153
.check-env:
@@ -167,3 +170,6 @@ docker-push:
167170
$(call log,DOCKER,Pushing $(IMAGE_NAME))
168171
@docker push $(IMAGE_NAME)
169172
$(call success,Push Complete)
173+
174+
integration-test:
175+
@$(MAKE) -C tests/integration test PYTEST_ARGS="$(PYTEST_ARGS)"

tests/integration/Makefile

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
4+
# -----------------------------------------------------------------------
5+
# Integration Test Makefile
6+
# -----------------------------------------------------------------------
7+
# Usage:
8+
# make test — Setup env + run pytest (PYTEST_ARGS="-k xxx")
9+
# make clean — Remove .venv and test output
10+
#
11+
# Prerequisites:
12+
# The FunctionStream binary must already be built (make build / make build-lite
13+
# from the project root).
14+
# -----------------------------------------------------------------------
15+
16+
PROJECT_ROOT := $(shell git -C $(CURDIR) rev-parse --show-toplevel)
17+
PYTHON_ROOT := $(PROJECT_ROOT)/python
18+
VENV := $(CURDIR)/.venv
19+
PIP := $(VENV)/bin/pip
20+
PY := $(VENV)/bin/python
21+
22+
C_G := \033[0;32m
23+
C_B := \033[0;34m
24+
C_0 := \033[0m
25+
26+
log = @printf "$(C_B)[-]$(C_0) %-12s %s\n" "$(1)" "$(2)"
27+
success = @printf "$(C_G)[✔]$(C_0) %s\n" "$(1)"
28+
29+
.PHONY: test clean help
30+
31+
help:
32+
@echo "Integration Test Targets:"
33+
@echo ""
34+
@echo " test Setup Python env + run pytest (PYTEST_ARGS=...)"
35+
@echo " clean Remove .venv and target/tests output"
36+
37+
$(VENV)/.installed: requirements.txt $(PYTHON_ROOT)/functionstream-api/pyproject.toml $(PYTHON_ROOT)/functionstream-client/pyproject.toml
38+
$(call log,ENV,Setting up Python virtual environment)
39+
@test -d $(VENV) || python3 -m venv $(VENV)
40+
@$(PIP) install --quiet --upgrade pip
41+
@$(PIP) install --quiet -r requirements.txt
42+
@$(PIP) install --quiet -e $(PYTHON_ROOT)/functionstream-api
43+
@$(PIP) install --quiet -e $(PYTHON_ROOT)/functionstream-client
44+
@touch $@
45+
$(call success,Python environment ready)
46+
47+
test: $(VENV)/.installed
48+
$(call log,TEST,Running integration tests)
49+
@$(PY) -m pytest -v $(PYTEST_ARGS)
50+
$(call success,All integration tests passed)
51+
52+
clean:
53+
$(call log,CLEAN,Removing test artifacts)
54+
@rm -rf $(VENV)
55+
@rm -rf $(CURDIR)/target
56+
$(call success,Clean complete)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
4+
from .instance import FunctionStreamInstance
5+
6+
__all__ = ["FunctionStreamInstance"]
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
4+
"""
5+
InstanceConfig: builds and writes the config.yaml consumed by
6+
the FunctionStream binary via FUNCTION_STREAM_CONF.
7+
"""
8+
9+
from pathlib import Path
10+
from typing import Any, Dict
11+
12+
import yaml
13+
14+
from .workspace import InstanceWorkspace
15+
16+
17+
class InstanceConfig:
18+
"""Generates and persists config.yaml for one FunctionStream instance."""
19+
20+
def __init__(self, host: str, port: int, workspace: InstanceWorkspace):
21+
self._workspace = workspace
22+
self._config: Dict[str, Any] = {
23+
"service": {
24+
"service_id": f"it-{port}",
25+
"service_name": "function-stream",
26+
"version": "1.0.0",
27+
"host": host,
28+
"port": port,
29+
"debug": False,
30+
},
31+
"logging": {
32+
"level": "info",
33+
"format": "json",
34+
"file_path": str(workspace.log_dir / "app.log"),
35+
"max_file_size": 50,
36+
"max_files": 3,
37+
},
38+
"state_storage": {
39+
"storage_type": "memory",
40+
},
41+
"task_storage": {
42+
"storage_type": "rocksdb",
43+
"db_path": str(workspace.data_dir / "task"),
44+
},
45+
"stream_catalog": {
46+
"persist": True,
47+
"db_path": str(workspace.data_dir / "stream_catalog"),
48+
},
49+
}
50+
51+
@property
52+
def raw(self) -> Dict[str, Any]:
53+
return self._config
54+
55+
def override(self, overrides: Dict[str, Any]) -> None:
56+
"""
57+
Apply overrides using dot-separated keys.
58+
Example: {"service.debug": True, "logging.level": "debug"}
59+
"""
60+
for dotted_key, value in overrides.items():
61+
keys = dotted_key.split(".")
62+
target = self._config
63+
for k in keys[:-1]:
64+
target = target.setdefault(k, {})
65+
target[keys[-1]] = value
66+
67+
def write_to_workspace(self) -> Path:
68+
"""Serialize config to the workspace config.yaml and return its path."""
69+
with open(self._workspace.config_file, "w", encoding="utf-8") as f:
70+
yaml.dump(self._config, f, default_flow_style=False, sort_keys=False)
71+
return self._workspace.config_file

0 commit comments

Comments
 (0)