Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/workflows/load-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Load Tests

on:
workflow_dispatch:
inputs:
update_baseline:
description: 'Update baselines after run'
required: false
default: false
type: boolean
fail_on_regression:
description: 'Fail if regression detected'
required: false
default: false
type: boolean

jobs:
load-test:
runs-on: ubuntu-latest
timeout-minutes: 60

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install uv
uses: astral-sh/setup-uv@v4

- name: Install dependencies
run: |
uv pip install --system -e ".[loadtest]"

- name: Build load test Docker image
run: |
docker build -f tests/Dockerfile.loadtest -t sse-starlette-loadtest:latest .

- name: Run load tests
run: |
python -m pytest tests/load/ -m "loadtest" \
--output-dir=tests/load/results \
${{ inputs.update_baseline && '--update-baseline' || '' }} \
${{ inputs.fail_on_regression && '--fail-on-regression' || '' }} \
-v --tb=short \
--junitxml=load-test-results.xml

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: load-test-results-${{ github.sha }}
path: |
load-test-results.xml
tests/load/results/*.json
tests/load/results/*.html
retention-days: 30

- name: Upload updated baselines
uses: actions/upload-artifact@v4
if: inputs.update_baseline
with:
name: updated-baselines-${{ github.sha }}
path: tests/load/baselines/*.json
retention-days: 90

- name: Test Summary
if: always()
run: |
echo "## Load Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Reports" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -d tests/load/results ]; then
for f in tests/load/results/*.json; do
if [ -f "$f" ]; then
echo "- $(basename "$f")" >> $GITHUB_STEP_SUMMARY
fi
done
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "Download artifacts for detailed HTML reports with charts." >> $GITHUB_STEP_SUMMARY
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ venv
Pipfile.lock
.envrc
.pdm-python

# Load test results (generated, not tracked)
tests/load/results/
!tests/load/results/.gitkeep
139 changes: 139 additions & 0 deletions .workmux.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# workmux project configuration
# For global settings, edit ~/.config/workmux/config.yaml
# All options below are commented out - uncomment to override defaults.

#-------------------------------------------------------------------------------
# Git
#-------------------------------------------------------------------------------

# The primary branch to merge into.
# Default: Auto-detected from remote HEAD, falls back to main/master.
# main_branch: main

# Default merge strategy for `workmux merge`.
# Options: merge (default), rebase, squash
# CLI flags (--rebase, --squash) always override this.
# merge_strategy: rebase

#-------------------------------------------------------------------------------
# Naming & Paths
#-------------------------------------------------------------------------------

# Directory where worktrees are created.
# Can be relative to repo root or absolute.
# Default: Sibling directory '<project>__worktrees'.
# worktree_dir: .worktrees

# Strategy for deriving names from branch names.
# Options: full (default), basename (part after last '/').
# worktree_naming: basename

# Prefix added to worktree directories and tmux window names.
# worktree_prefix: ""

# Prefix for tmux window names.
# Default: "wm-"
# window_prefix: "wm-"

#-------------------------------------------------------------------------------
# Tmux
#-------------------------------------------------------------------------------

# Custom tmux pane layout.
# Default: Two-pane layout with shell and clear command.
# panes:
# - command: pnpm install
# focus: true
# - split: horizontal
# - command: clear
# split: vertical
# size: 5

# Auto-apply agent status icons to tmux window format.
# Default: true
# status_format: true

# Custom icons for agent status display.
status_icons:
working: "🤖"
waiting: "💬"
done: "✅"

#-------------------------------------------------------------------------------
# Agent & AI
#-------------------------------------------------------------------------------

# Agent command for '<agent>' placeholder in pane commands.
# Default: "claude"
# agent: claude

# LLM-based branch name generation (`workmux add -a`).
# auto_name:
# model: "gpt-4o-mini"
# system_prompt: "Generate a kebab-case git branch name."

#-------------------------------------------------------------------------------
# Hooks
#-------------------------------------------------------------------------------

# Commands to run in new worktree before tmux window opens.
# These block window creation - use for short tasks only.
# Use "<global>" to inherit from global config.
# Set to empty list to disable: `post_create: []`
# post_create:
# - "<global>"
# - mise use

# ẞOTCHA: copies .envrc target, not link
post_create:
- ln -s $SOPS_PATH/dot.envrc .envrc
- direnv allow

# Commands to run before merging (e.g., linting, tests).
# Aborts the merge if any command fails.
# Use "<global>" to inherit from global config.
# Environment variables available:
# - WM_BRANCH_NAME: The name of the branch being merged
# - WM_TARGET_BRANCH: The name of the target branch (e.g., main)
# - WM_WORKTREE_PATH: Absolute path to the worktree
# - WM_PROJECT_ROOT: Absolute path of the main project directory
# - WM_HANDLE: The worktree handle/window name
# pre_merge:
# - "<global>"
# - cargo test
# - cargo clippy -- -D warnings

# Commands to run before worktree removal (during merge or remove).
# Useful for backing up gitignored files before cleanup.
# Default: Auto-detects Node.js projects and fast-deletes node_modules.
# Set to empty list to disable: `pre_remove: []`
# Environment variables available:
# - WM_HANDLE: The worktree handle (directory name)
# - WM_WORKTREE_PATH: Absolute path of the worktree being deleted
# - WM_PROJECT_ROOT: Absolute path of the main project directory
# pre_remove:
# - mkdir -p "$WM_PROJECT_ROOT/artifacts/$WM_HANDLE"
# - cp -r test-results/ "$WM_PROJECT_ROOT/artifacts/$WM_HANDLE/"

#-------------------------------------------------------------------------------
# Files
#-------------------------------------------------------------------------------

# File operations when creating a worktree.
# files:
# # Files to copy (useful for .env files that need to be unique).
# copy:
# - .env.local
#
# # Files/directories to symlink (saves disk space, shares caches).
# # Default: None.
# # Use "<global>" to inherit from global config.
# symlink:
# - "<global>"
# - node_modules
files:
symlink:
- .venv
- .claude
- CLAUDE.md
- thoughts
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ test: test-unit test-docker ## run tests

.PHONY: test-unit
test-unit: ## run all tests except "integration" marked
RUN_ENV=local python -m pytest -m "not (integration or experimentation)" --cov-config=pyproject.toml --cov-report=html --cov-report=term --cov=$(pkg_src) tests
RUN_ENV=local python -m pytest -m "not (integration or experimentation or loadtest)" --cov-config=pyproject.toml --cov-report=html --cov-report=term --cov=$(pkg_src) tests

.PHONY: test-docker
test-docker: ## test-docker (docker desktop: advanced settings)
Expand All @@ -100,6 +100,17 @@ test-docker: ## test-docker (docker desktop: advanced settings)
echo "Skipping tests: /var/run/docker.sock does not exist."; \
fi

.PHONY: test-load
test-load: ## run load tests (requires docker, make test-load PYTEST_ARGS="--scale=500 --duration=5")
@if [ -S /var/run/docker.sock > /dev/null 2>&1 ]; then \
echo "Building load test image..."; \
docker build -f tests/Dockerfile.loadtest -t sse-starlette-loadtest:latest .; \
echo "Running load tests..."; \
RUN_ENV=local python -m pytest -m "loadtest" tests/load/ -v --tb=short $(PYTEST_ARGS); \
else \
echo "Skipping load tests: /var/run/docker.sock does not exist."; \
fi


################################################################################
# Code Quality \
Expand Down
File renamed without changes.
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,17 @@ granian = [
daphne = [
"daphne>=4.2.0",
]
loadtest = [
"httpx-sse>=0.4.0",
"psutil>=6.1.1",
]

[dependency-groups] # new standard, included by default
dev = [
"asgi-lifespan>=2.1.0",
"async-timeout>=5.0.1",
"httpx>=0.28.1",
"httpx-sse>=0.4.0",
"mypy>=1.14.0",
"portend>=3.2.0",
"psutil>=6.1.1",
Expand Down Expand Up @@ -102,7 +107,8 @@ filename = "sse_starlette/__init__.py"
[tool.pytest.ini_options]
markers = [
"integration: marks tests as integration tests",
"experimentation: marks tests as experimental tests, not to be run in CICD"
"experimentation: marks tests as experimental tests, not to be run in CICD",
"loadtest: marks tests as load tests (require docker and significant resources)"
]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
Expand Down
32 changes: 32 additions & 0 deletions tests/Dockerfile.loadtest
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Load test server image for sse-starlette
FROM python:3.12-slim

WORKDIR /app

# Install build dependencies and cleanup in one layer
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*

# Copy package files
COPY pyproject.toml ./
COPY README.md ./
COPY sse_starlette ./sse_starlette

# Install package with loadtest dependencies
RUN pip install --no-cache-dir -e ".[loadtest]"

# Install uvicorn for serving
RUN pip install --no-cache-dir uvicorn

# Copy load test server app
COPY tests/load/server_app.py ./server_app.py

# Expose port
EXPOSE 8000

# Set Python path
ENV PYTHONPATH=/app

# Default command - run the load test server
CMD ["uvicorn", "server_app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
Loading
Loading