Repository Testing Fixtures Guide¶
This page documents pytest fixtures used by the errorworks repository test suite. They live under tests/fixtures, outside src/errorworks, and are not included in the installed wheel. Use these helpers when developing or maintaining errorworks from a source checkout.
ChaosLLM, ChaosWeb, and ChaosBlob repository fixtures run in-process using Starlette's TestClient, so no real network sockets are opened for HTTP tests. The ChaosSMTP repository fixture uses an ephemeral loopback TCP socket because normal SMTP clients require a real socket and protocol session.
Setup¶
From the errorworks source tree, import the fixtures in the relevant test package conftest.py:
# tests/conftest.py
from tests.fixtures.chaosllm import chaosllm_server # noqa: F401
from tests.fixtures.chaossmtp import chaossmtp_server # noqa: F401
from tests.fixtures.chaosweb import chaosweb_server # noqa: F401
from tests.fixtures.chaosblob import chaosblob # noqa: F401
Register the custom markers to avoid pytest warnings:
# tests/conftest.py (or pyproject.toml)
def pytest_configure(config):
config.addinivalue_line("markers", "chaosllm: ChaosLLM server configuration")
config.addinivalue_line("markers", "chaossmtp: ChaosSMTP server configuration")
config.addinivalue_line("markers", "chaosweb: ChaosWeb server configuration")
config.addinivalue_line("markers", "chaosblob: ChaosBlob server configuration")
ChaosLLM Fixture¶
Basic Usage¶
def test_successful_completion(chaosllm_server):
"""Test that completions work with no error injection."""
response = chaosllm_server.post_completion(
messages=[{"role": "user", "content": "Hello"}],
model="gpt-4",
)
assert response.status_code == 200
data = response.json()
assert "choices" in data
assert data["choices"][0]["message"]["content"]
Marker-Based Configuration¶
Use @pytest.mark.chaosllm(...) to configure the server for a specific test:
import pytest
@pytest.mark.chaosllm(preset="realistic", rate_limit_pct=25.0)
def test_rate_limit_handling(chaosllm_server):
"""Test that the pipeline handles rate limits."""
errors = 0
for _ in range(100):
response = chaosllm_server.post_completion()
if response.status_code == 429:
errors += 1
# With 25% rate limiting, expect roughly 25 errors out of 100
assert errors > 10
Available Marker Kwargs¶
The chaosllm marker accepts these keyword arguments:
| Kwarg | Type | Description |
|---|---|---|
preset |
str | Base preset name (silent, gentle, realistic, etc.) |
rate_limit_pct |
float | 429 Rate Limit percentage |
capacity_529_pct |
float | 529 Capacity error percentage |
service_unavailable_pct |
float | 503 Service Unavailable percentage |
bad_gateway_pct |
float | 502 Bad Gateway percentage |
gateway_timeout_pct |
float | 504 Gateway Timeout percentage |
internal_error_pct |
float | 500 Internal Server Error percentage |
timeout_pct |
float | Timeout (hang) percentage |
connection_reset_pct |
float | Connection reset percentage |
connection_failed_pct |
float | Connection failed percentage |
connection_stall_pct |
float | Connection stall percentage |
slow_response_pct |
float | Slow response percentage |
invalid_json_pct |
float | Invalid JSON response percentage |
truncated_pct |
float | Truncated response percentage |
empty_body_pct |
float | Empty body response percentage |
missing_fields_pct |
float | Missing fields response percentage |
wrong_content_type_pct |
float | Wrong Content-Type percentage |
forbidden_pct |
float | 403 Forbidden percentage |
not_found_pct |
float | 404 Not Found percentage |
selection_mode |
str | priority or weighted |
base_ms |
int | Base latency in milliseconds |
jitter_ms |
int | Latency jitter in milliseconds |
mode |
str | Response mode (random, template, echo, preset) |
Note
The fixture sets base_ms=0 and jitter_ms=0 by default so tests run without artificial delays. If you need latency simulation, set these explicitly in the marker.
Fixture Helpers¶
The ChaosLLMFixture object provides:
| Method/Property | Description |
|---|---|
post_completion(messages=..., model=..., **kwargs) |
POST to /v1/chat/completions |
post_azure_completion(deployment, messages=..., **kwargs) |
POST to Azure endpoint |
get_stats() |
Get metrics summary (same as /admin/stats) |
export_metrics() |
Export raw metrics data |
update_config(rate_limit_pct=..., ...) |
Update config at runtime |
reset() |
Reset metrics and start new run |
wait_for_requests(count, timeout=10.0) |
Block until N requests recorded |
run_id |
Current run ID |
url |
Base URL (http://testserver) |
admin_headers |
Dict with auth headers for admin endpoints |
Azure Endpoint Testing¶
def test_azure_deployment(chaosllm_server):
"""Test Azure OpenAI endpoint compatibility."""
response = chaosllm_server.post_azure_completion(
deployment="my-gpt4-deployment",
messages=[{"role": "user", "content": "Hello"}],
api_version="2024-02-01",
)
assert response.status_code == 200
ChaosWeb Fixture¶
Basic Usage¶
def test_page_fetch(chaosweb_server):
"""Test that pages load successfully."""
response = chaosweb_server.fetch_page("/articles/test")
assert response.status_code == 200
assert "html" in response.text.lower()
Marker-Based Configuration¶
import pytest
@pytest.mark.chaosweb(preset="stress_scraping", rate_limit_pct=25.0)
def test_scraper_resilience(chaosweb_server):
"""Test scraper handles rate limiting under stress."""
success = 0
for _ in range(50):
response = chaosweb_server.fetch_page("/articles/test")
if response.status_code == 200:
success += 1
assert success > 0 # At least some succeed
Available Marker Kwargs¶
The chaosweb marker accepts these keyword arguments:
| Kwarg | Type | Description |
|---|---|---|
preset |
str | Base preset name |
rate_limit_pct |
float | 429 Rate Limit percentage |
forbidden_pct |
float | 403 Forbidden percentage |
not_found_pct |
float | 404 Not Found percentage |
gone_pct |
float | 410 Gone percentage |
payment_required_pct |
float | 402 Payment Required percentage |
unavailable_for_legal_pct |
float | 451 Unavailable for Legal Reasons percentage |
service_unavailable_pct |
float | 503 Service Unavailable percentage |
bad_gateway_pct |
float | 502 Bad Gateway percentage |
gateway_timeout_pct |
float | 504 Gateway Timeout percentage |
internal_error_pct |
float | 500 Internal Server Error percentage |
timeout_pct |
float | Timeout percentage |
connection_reset_pct |
float | Connection reset percentage |
connection_stall_pct |
float | Connection stall percentage |
slow_response_pct |
float | Slow response percentage |
incomplete_response_pct |
float | Incomplete response percentage |
wrong_content_type_pct |
float | Wrong Content-Type percentage |
encoding_mismatch_pct |
float | Encoding mismatch percentage |
truncated_html_pct |
float | Truncated HTML percentage |
invalid_encoding_pct |
float | Invalid encoding percentage |
charset_confusion_pct |
float | Charset confusion percentage |
malformed_meta_pct |
float | Malformed meta tag percentage |
redirect_loop_pct |
float | Redirect loop percentage |
ssrf_redirect_pct |
float | SSRF redirect percentage |
selection_mode |
str | priority or weighted |
base_ms |
int | Base latency in milliseconds |
jitter_ms |
int | Latency jitter in milliseconds |
content_mode |
str | Content mode (random, template, echo, preset) |
Fixture Helpers¶
The ChaosWebFixture object provides:
| Method/Property | Description |
|---|---|
fetch_page(path="/", headers=..., follow_redirects=False) |
GET a page |
get_stats() |
Get metrics summary |
export_metrics() |
Export raw metrics data |
update_config(rate_limit_pct=..., ...) |
Update config at runtime |
reset() |
Reset metrics and start new run |
wait_for_requests(count, timeout=10.0) |
Block until N requests recorded |
run_id |
Current run ID |
base_url |
Base URL (http://testserver) |
admin_headers |
Dict with auth headers for admin endpoints |
ChaosBlob Fixture¶
Basic Usage¶
def test_object_round_trip(chaosblob):
"""Test a simple object-storage round trip."""
put = chaosblob.put_object(
"bucket",
"incoming/item.json",
b'{"id": 1}',
headers={"content-type": "application/json"},
)
assert put.status_code == 200
get = chaosblob.get_object("bucket", "incoming/item.json")
assert get.status_code == 200
assert get.json() == {"id": 1}
assert get.headers["etag"] == put.headers["etag"]
Marker-Based Configuration¶
import pytest
@pytest.mark.chaosblob(preset="silent", slow_down_pct=100.0)
def test_blob_retry_on_slow_down(chaosblob):
"""Test that the pipeline handles object-store throttling."""
response = chaosblob.put_object("bucket", "key", b"data")
assert response.status_code == 503
assert "retry-after" in response.headers
chaosblob.update_config(slow_down_pct=0.0)
assert chaosblob.put_object("bucket", "key", b"data").status_code == 200
Available Marker Kwargs¶
The chaosblob marker accepts these keyword arguments:
| Kwarg | Type | Description |
|---|---|---|
preset |
str | Base preset name (silent, gentle, realistic, etc.) |
slow_down_pct |
float | S3 SlowDown percentage |
access_denied_pct |
float | 403 AccessDenied percentage |
not_found_pct |
float | 404 NoSuchKey percentage |
service_unavailable_pct |
float | 503 ServiceUnavailable percentage |
internal_error_pct |
float | 500 InternalError percentage |
bad_gateway_pct |
float | 502 BadGateway percentage |
gateway_timeout_pct |
float | 504 GatewayTimeout percentage |
timeout_pct |
float | Timeout percentage |
connection_reset_pct |
float | Connection reset percentage |
connection_stall_pct |
float | Connection stall percentage |
slow_response_pct |
float | Slow response percentage |
truncated_body_pct |
float | Truncated object body percentage |
wrong_content_length_pct |
float | Wrong Content-Length percentage |
checksum_mismatch_pct |
float | ETag/checksum mismatch percentage |
metadata_corruption_pct |
float | Metadata corruption percentage |
stale_list_pct |
float | Stale list response percentage |
malformed_xml_pct |
float | Malformed XML response percentage |
selection_mode |
str | priority or weighted |
base_ms |
int | Base latency in milliseconds |
jitter_ms |
int | Latency jitter in milliseconds |
max_object_bytes |
int | Maximum stored object size |
Unknown marker kwargs fail fast so misspelled settings do not silently turn into happy-path tests.
Note
The fixture sets base_ms=0 and jitter_ms=0 by default so tests run without artificial delays. If you need latency simulation, set these explicitly in the marker.
Fixture Helpers¶
The ChaosBlobFixture object provides:
| Method/Property | Description |
|---|---|
put_object(bucket, key, body, headers=...) |
PUT object bytes |
get_object(bucket, key, headers=...) |
GET object bytes |
head_object(bucket, key, headers=...) |
HEAD object metadata |
delete_object(bucket, key) |
DELETE an object |
list_objects(bucket, prefix="", max_keys=1000) |
List objects through ListObjectsV2 |
get_stats() |
Get metrics summary |
export_metrics() |
Export raw metrics data |
update_config(slow_down_pct=..., updates=..., ...) |
Update config at runtime |
reset() |
Reset metrics, stored objects, and start a new run |
wait_for_requests(count, timeout=10.0) |
Block until N requests recorded |
run_id |
Current run ID |
base_url |
Base URL (http://testserver) |
admin_headers |
Dict with auth headers for admin endpoints |
ChaosSMTP Fixture¶
The repository's chaossmtp_server fixture starts a real SMTP listener on 127.0.0.1 with smtp.port=0, so the operating system assigns an ephemeral port. This keeps tests isolated while still exercising standard clients like smtplib.
Basic Usage¶
from email.message import EmailMessage
def test_sends_message(chaossmtp_server):
message = EmailMessage()
message["From"] = "sender@example.com"
message["To"] = "recipient@example.com"
message["Subject"] = "Fixture test"
message.set_content("hello")
assert chaossmtp_server.send_message(message) == {}
assert chaossmtp_server.wait_for_messages(1)
assert chaossmtp_server.get_stats()["total_requests"] == 1
Marker-Based Configuration¶
from email.message import EmailMessage
import pytest
import smtplib
@pytest.mark.chaossmtp(rcpt_to_reject_pct=100.0)
def test_recipient_rejection(chaossmtp_server):
message = EmailMessage()
message["From"] = "sender@example.com"
message["To"] = "recipient@example.com"
message["Subject"] = "Rejected"
message.set_content("hello")
with pytest.raises(smtplib.SMTPRecipientsRefused):
chaossmtp_server.send_message(message)
Available Marker Kwargs¶
The chaossmtp marker accepts these keyword arguments:
| Kwarg | Type | Description |
|---|---|---|
preset |
str | Base preset name (silent, gentle, realistic, stress_delivery, stress_extreme) |
rate_limit_pct |
float | SMTP rate limit percentage |
mail_from_tempfail_pct |
float | MAIL FROM temporary failure percentage |
mail_from_reject_pct |
float | MAIL FROM permanent rejection percentage |
rcpt_to_tempfail_pct |
float | RCPT TO temporary failure percentage |
rcpt_to_reject_pct |
float | RCPT TO permanent rejection percentage |
data_tempfail_pct |
float | DATA temporary failure percentage |
data_reject_pct |
float | DATA permanent rejection percentage |
accept_then_drop_pct |
float | Accept message and drop without capture percentage |
malformed_reply_pct |
float | Malformed SMTP reply percentage |
wrong_reply_code_pct |
float | Unexpected SMTP reply code percentage |
connection_reset_pct |
float | SMTP transport close percentage |
connection_stall_pct |
float | Stall then close percentage |
slow_response_pct |
float | Slow response percentage |
retry_after_sec |
tuple[int, int] | Validated retry range config; current SMTP replies do not emit a Retry-After header |
connection_stall_sec |
tuple[int, int] | Stall duration range |
slow_response_sec |
tuple[int, int] | Slow response delay range |
selection_mode |
str | priority or weighted |
base_ms |
int | Base latency in milliseconds |
jitter_ms |
int | Latency jitter in milliseconds |
capture_mode |
str | Capture mode (discard, metadata, full) |
max_message_bytes |
int | Maximum bytes stored in full capture mode |
max_messages |
int | Maximum captured messages kept in memory |
Note
The fixture sets smtp.port=0, base_ms=0, jitter_ms=0, and a deterministic admin token by default. If you need latency simulation, set latency explicitly in the marker.
Fixture Helpers¶
The ChaosSMTPFixture object provides:
| Method/Property | Description |
|---|---|
send_message(message) |
Send an EmailMessage with smtplib.SMTP |
get_stats() |
Get metrics summary |
export_metrics() |
Export raw metrics data and captured messages |
update_config(rcpt_to_tempfail_pct=..., capture_mode=..., ...) |
Update runtime config |
reset() |
Reset metrics and captured messages |
wait_for_requests(count, timeout=10.0) |
Block until N SMTP transactions are recorded |
wait_for_messages(count, timeout=10.0) |
Block until N messages are captured |
host |
Bound SMTP host |
port |
Ephemeral SMTP port |
metrics_db |
File-backed SQLite metrics path for this test |
run_id |
Current run ID |
Complete Working Examples¶
ChaosLLM: Testing Error Recovery¶
import pytest
@pytest.mark.chaosllm(rate_limit_pct=100.0)
def test_all_requests_rate_limited(chaosllm_server):
"""Verify behavior when every request is rate limited."""
response = chaosllm_server.post_completion()
assert response.status_code == 429
assert "Retry-After" in response.headers
data = response.json()
assert data["error"]["type"] == "rate_limit_error"
@pytest.mark.chaosllm(invalid_json_pct=100.0)
def test_malformed_json_handling(chaosllm_server):
"""Verify the client can detect invalid JSON responses."""
response = chaosllm_server.post_completion()
assert response.status_code == 200 # Malformed responses return 200
# The body is not valid JSON
try:
response.json()
assert False, "Expected JSON decode error"
except Exception:
pass # Expected
def test_runtime_config_update(chaosllm_server):
"""Verify runtime config updates take effect."""
# Start with no errors
response = chaosllm_server.post_completion()
assert response.status_code == 200
# Enable 100% rate limiting
chaosllm_server.update_config(rate_limit_pct=100.0)
response = chaosllm_server.post_completion()
assert response.status_code == 429
# Check metrics
stats = chaosllm_server.get_stats()
assert stats["total_requests"] == 2
def test_metrics_tracking(chaosllm_server):
"""Verify metrics are recorded for each request."""
for _ in range(10):
chaosllm_server.post_completion()
chaosllm_server.wait_for_requests(10)
stats = chaosllm_server.get_stats()
assert stats["total_requests"] == 10
assert "latency_stats" in stats
ChaosWeb: Testing Scraper Resilience¶
import pytest
@pytest.mark.chaosweb(forbidden_pct=100.0)
def test_all_requests_blocked(chaosweb_server):
"""Verify behavior when bot detection blocks everything."""
response = chaosweb_server.fetch_page("/articles/test")
assert response.status_code == 403
@pytest.mark.chaosweb(encoding_mismatch_pct=100.0)
def test_encoding_mismatch_detection(chaosweb_server):
"""Verify the scraper detects encoding mismatches."""
response = chaosweb_server.fetch_page("/articles/test")
assert response.status_code == 200
# Header says UTF-8 but body is ISO-8859-1
assert "utf-8" in response.headers.get("content-type", "").lower()
@pytest.mark.chaosweb(redirect_loop_pct=100.0)
def test_redirect_loop_handling(chaosweb_server):
"""Verify the scraper detects redirect loops."""
response = chaosweb_server.fetch_page("/articles/test")
assert response.status_code == 301
assert "Location" in response.headers
# Without follow_redirects, you get the first redirect
assert "/redirect?" in response.headers["Location"]
@pytest.mark.chaosweb(preset="realistic")
def test_realistic_scraping(chaosweb_server):
"""Test with realistic error distribution."""
results = {"success": 0, "error": 0}
for _ in range(100):
response = chaosweb_server.fetch_page("/articles/test")
if response.status_code == 200:
results["success"] += 1
else:
results["error"] += 1
# Realistic preset: ~80% success rate
assert results["success"] > 50
ChaosBlob: Testing Blob Pipeline Recovery¶
import pytest
@pytest.mark.chaosblob(preset="silent")
def test_blob_pipeline_handles_slow_down(chaosblob):
"""Verify a blob pipeline can retry after object-store throttling."""
chaosblob.put_object("bucket", "incoming/1.json", b'{"id": 1}')
chaosblob.update_config(slow_down_pct=100.0, updates={"error_injection": {"retry_after_sec": [0, 0]}})
response = chaosblob.get_object("bucket", "incoming/1.json")
assert response.status_code == 503
assert response.headers["retry-after"] == "0"
chaosblob.update_config(slow_down_pct=0.0)
response = chaosblob.get_object("bucket", "incoming/1.json")
assert response.status_code == 200
assert response.json() == {"id": 1}
stats = chaosblob.get_stats()
assert stats["total_requests"] == 3
ChaosSMTP: Testing Delivery Failures¶
from email.message import EmailMessage
import pytest
import smtplib
def _message():
message = EmailMessage()
message["From"] = "sender@example.com"
message["To"] = "recipient@example.com"
message["Subject"] = "Delivery test"
message.set_content("hello")
return message
@pytest.mark.chaossmtp(data_reject_pct=100.0)
def test_data_rejection(chaossmtp_server):
"""Verify behavior when every DATA command is rejected."""
with pytest.raises(smtplib.SMTPDataError) as exc_info:
chaossmtp_server.send_message(_message())
assert exc_info.value.smtp_code == 554
def test_capture_metadata(chaossmtp_server):
"""Verify accepted messages are captured as metadata by default."""
assert chaossmtp_server.send_message(_message()) == {}
assert chaossmtp_server.wait_for_messages(1)
exported = chaossmtp_server.export_metrics()
assert exported["messages"][0]["subject"] == "Delivery test"
How It Works¶
The ChaosLLM and ChaosWeb repository fixtures use Starlette's TestClient, which wraps the ASGI application and routes HTTP calls through the stack without opening a network socket. This means:
- No port conflicts -- multiple tests can run in parallel
- No startup delay -- the server is ready immediately
- Full fidelity -- the same request handling code runs as in production
- Isolated state -- each test gets a fresh server instance via
tmp_path
The ChaosSMTP repository fixture uses the same configuration pattern, but it starts a real aiosmtpd listener on an ephemeral loopback port. That keeps tests close to production SMTP client behavior while avoiding fixed-port conflicts.
The _build_config_from_marker() function translates marker kwargs into a ChaosLLMConfig, ChaosWebConfig, ChaosBlobConfig, or ChaosSMTPConfig object. It applies the same precedence rules as the CLI: marker kwargs override the preset, and the fixture always forces latency to zero and sets a deterministic admin token for test convenience.
Related Pages¶
- ChaosLLM -- Error types and response modes
- ChaosWeb -- Error types and content modes
- ChaosBlob -- Object-storage error types and fixture helpers
- ChaosSMTP -- SMTP listener, error types, and capture modes
- Metrics -- Understanding metrics data in tests
- Configuration -- How configuration precedence works