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
66 changes: 25 additions & 41 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ name: CI
#
# gate1 lint ruff
# gate2 types mypy
# gate3 unit tests + coverage pytest (Docker-free suite)
# gate3 unit tests + coverage pytest (Docker-free suite), floor 77%
# gate4 contract validation JSON Schema round-trip over the fixtures
# gate5 app boot smoke API and worker import without a database
# gate6 migration contribution COM07 contribution-root contract
# gate7 dependency + secret scan pip-audit + TruffleHog
# gate8 DB integration Testcontainers PostgreSQL 16
# gate9 object-store integration LocalStack S3
# gate8 DB integration Testcontainers PostgreSQL 16 + LocalStack
# gate9 object-store integration Testcontainers LocalStack S3

on:
pull_request:
Expand Down Expand Up @@ -99,13 +99,19 @@ jobs:
# stdout is a pipe, so the kill discarded every line and the log named no
# test. The same command finishes locally in ~16s. Unbuffered output plus
# a per-test timeout turn a hang into a named test with a stack trace.
#
# The coverage floor is the measured total (statements plus branches),
# rounded down: 77.6% on develop at a568df0. data-pipeline holds 88; this
# repository is not there yet, so the floor only ratchets upward. Raise it
# when coverage grows and never lower it to make a change pass.
- name: pytest (Docker-free suite)
env:
PYTHONUNBUFFERED: "1"
run: >-
python -m pytest -p no:cacheprovider -m "not docker"
--timeout=120 --timeout-method=thread
--cov=backtest_engine --cov-report=term-missing --cov-report=xml
--cov-fail-under=77
- name: Upload coverage
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down Expand Up @@ -291,27 +297,14 @@ jobs:
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 30
# LocalStack as well as PostgreSQL, because `-m docker` is a single set that
# spans both: the worker's SQS tests and the end-to-end test need a queue and
# an object store, not just a database. Without this the queue tests skipped
# here - and their default endpoint is a developer's local container port, so
# they passed on that machine and covered nothing in CI.
services:
localstack:
image: localstack/localstack:4.7.0
env:
SERVICES: s3,sqs
EAGER_SERVICE_LOADING: "1"
AWS_DEFAULT_REGION: us-east-1
ports:
- 4566:4566
options: >-
--health-cmd "curl -sf http://localhost:4566/_localstack/health || exit 1"
--health-interval 10s --health-timeout 5s --health-retries 12
# `tests/conftest.py` starts its own PostgreSQL 16 container through
# Testcontainers and applies the CENTRAL Flyway bundle to it, so there is no
# `services:` block here: a second, differently-migrated server would only
# invite tests to point at the wrong one.
# `-m docker` is a single set that spans a database, a queue and an object
# store: the worker's SQS tests and the end-to-end tests need all three.
# `tests/conftest.py` starts every one of them itself through Testcontainers:
# a PostgreSQL 16 container migrated with the CENTRAL Flyway bundle, and one
# LocalStack (SQS + S3) for the session. There is deliberately no `services:`
# block here: nothing in the suite reads a fixed endpoint, so a service
# container would run unused, and a second, differently-migrated database
# would only invite tests to point at the wrong one.
#
# That harness *skips* when Docker is unavailable, and a skip is a silent
# pass. The last step therefore asserts, from the JUnit report, that the
Expand All @@ -336,13 +329,11 @@ jobs:
# The container is migrated with the vendored byte-for-byte copy of the
# applied central Flyway bundle, so this proves the persistence layer
# against the canonical schema and not against a restatement of it.
- name: Integration tests (PostgreSQL 16 via Testcontainers, queue via LocalStack)
- name: Integration tests (PostgreSQL 16 and LocalStack via Testcontainers)
env:
# tests/test_worker.py defaults to a developer's local LocalStack port
# and skips when it is absent. Point it at the service container so the
# real SQS coverage actually executes here.
BACKTEST_TEST_SQS_ENDPOINT: http://localhost:4566
LOCALSTACK_ENDPOINT_URL: http://localhost:4566
# Dummy credentials. Every boto3 client in the suite carries its own
# endpoint and credentials and passes without these; they only make
# sure a stray default-chain client can never pick up real ones.
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1
Expand Down Expand Up @@ -387,16 +378,9 @@ jobs:
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 20
services:
localstack:
image: localstack/localstack:4.7.0
env:
SERVICES: s3,sqs
ports:
- 4566:4566
options: >-
--health-cmd "curl -sf http://localhost:4566/_localstack/health || exit 1"
--health-interval 10s --health-timeout 5s --health-retries 12
# No `services:` block: the S3 leg starts its own LocalStack through
# Testcontainers (`tests/conftest.py` and `tests/test_object_store.py`) and
# reads no fixed endpoint, so a service container here would run unused.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
Expand All @@ -410,7 +394,7 @@ jobs:
# runs against both backends, and the S3 leg must not skip here.
- name: Object-store contract against LocalStack S3
env:
LOCALSTACK_ENDPOINT_URL: http://localhost:4566
# Dummy credentials, for the same reason as in gate8.
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ dependencies = [
]

[project.optional-dependencies]
# `httpx` (not the non-existent `httpx2`) is what starlette.testclient imports.
# starlette.testclient prefers `httpx2` and falls back to this `httpx` with a
# StarletteDeprecationWarning. Moving to `httpx2` is a separate, deliberate bump.
test = [
"freezegun==1.5.5",
"hypothesis==6.138.15",
Expand Down
4 changes: 4 additions & 0 deletions tests/test_attempt_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,10 @@ def _run(conn: Any) -> None:
return applied


@pytest.mark.skipif(
sys.platform == "darwin",
reason="macOS rejects setrlimit(RLIMIT_AS); the Linux worker image is what enforces it",
)
def test_monitor_names_the_enforcement_mechanism_actually_in_force() -> None:
if sys.platform == "win32":
# Windows has no POSIX rlimit, so the call is a no-op on this process
Expand Down
Loading