Testing¶
Unit tests (no database, no network)¶
pip install -e ".[dev]"
pytest -q # all tests; integration tests skip automatically
ruff check && ruff format --check
The default run needs neither a database nor the network.
In-process sinks (SQLite, DuckDB)¶
SQLite uses the standard-library sqlite3 module and DuckDB runs in-process, so both contract
suites execute on every pytest run when the driver is installed:
pytest -q tests/test_sqlite_integration.py tests/test_duckdb_integration.py
To exercise them through the multi-sink dispatch path:
export DATABASE_TARGET=postgres,sqlite
export POSTGRES_DSN=postgresql://hkex:hkex@localhost:5432/hkex
export SQLITE_PATH=/tmp/hkex-it.db
pytest -q
Server-backed sinks¶
These suites are skipped unless the matching connection variables are set. Each needs its driver installed.
PostgreSQL¶
export DATABASE_TARGET=postgres
export POSTGRES_DSN=postgresql://hkex:hkex@localhost:5432/hkex
pip install -e ".[dev,postgres]"
pytest -q tests/test_postgres_integration.py
MySQL / MariaDB¶
export DATABASE_TARGET=mysql
export MYSQL_HOST=localhost MYSQL_DATABASE=hkex MYSQL_USER=hkex MYSQL_PASSWORD=hkex
pip install -e ".[dev,mysql]"
pytest -q tests/test_mysql_integration.py
SurrealDB¶
export DATABASE_TARGET=surrealdb
export SURREAL_ENDPOINT=http://localhost:8000
export SURREAL_NAMESPACE=test
export SURREAL_DATABASE=hkex
export SURREAL_USERNAME=root
export SURREAL_PASSWORD=root
export COMPANY_TABLE=company # enables the graph-edge test
pytest -q tests/test_surrealdb_integration.py
A throwaway SurrealDB (in-memory):
docker run -d --name hkex-surreal -p 8000:8000 \
surrealdb/surrealdb:v3.2.4 start --log warn --user root --pass root --bind 0.0.0.0:8000 memory
Sinks (MongoDB, ClickHouse, Neo4j)¶
export DATABASE_TARGET=mongodb,clickhouse,neo4j
export MONGODB_URI=mongodb://localhost:27017
export MONGODB_DATABASE=hkex
export CLICKHOUSE_HOST=localhost CLICKHOUSE_PORT=8123 CLICKHOUSE_DATABASE=hkex
export CLICKHOUSE_USER=hkex CLICKHOUSE_PASSWORD=hkex
export NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=hkexpassword
pip install -e ".[dev,duckdb,mongodb,clickhouse,neo4j]"
pytest -q tests/test_extra_sinks_integration.py
Multi-sink integration¶
export DATABASE_TARGET=postgres,surrealdb,sqlite
export SURREAL_ENDPOINT=http://localhost:8000
export SURREAL_NAMESPACE=test
export SURREAL_DATABASE=hkex
export SURREAL_USERNAME=root
export SURREAL_PASSWORD=root
export POSTGRES_DSN=postgresql://hkex:hkex@localhost:5432/hkex
export SQLITE_PATH=/tmp/hkex-it.db
pytest -q tests/test_postgres_integration.py tests/test_surrealdb_integration.py tests/test_dual_write_integration.py
Every configured sink must have its schema initialized; the integration fixtures do this for
the whole configured set (matching main._init_schemas()).
What is covered¶
| Test file | Requires | Checks |
|---|---|---|
tests/test_utils.py |
nothing | helpers, classification, ticker extraction, company-id loading (incl. missing table) |
tests/test_coverage.py |
nothing | coverage statement/logging shape |
tests/test_config.py |
nothing | DATABASE_TARGET parsing, CLI override, MySQL/PostgreSQL connection helpers |
tests/test_registry.py |
nothing | lazy driver loading, valid ids, capability declarations, read routing |
tests/test_dialects.py |
nothing | per-dialect DDL, upsert/edge/coverage SQL, value encoding |
tests/test_relational_sink.py |
nothing | driver-missing/unconfigured degradation, credential redaction |
tests/test_postgres_sink.py |
nothing | PostgreSQL DDL, upserts, degradation, redaction, adapter |
tests/test_sink_dispatch.py |
nothing | multi-sink dispatch, failure isolation and counters, graph dispatch, parity report |
tests/test_docs_consistency.py |
nothing | every sink id appears in the docs, templates, and .env.example; DATABASE_TARGET rows list every id |
tests/test_sqlite_integration.py |
nothing | full SQLite contract: schema, idempotent upserts, no-clobber, edges, reads |
tests/test_extra_sinks.py |
nothing | MongoDB/ClickHouse/Neo4j capabilities, degradation, ClickHouse merge, Neo4j Cypher |
tests/test_duckdb_integration.py |
duckdb installed |
full DuckDB contract in-process |
tests/test_extra_sinks_integration.py |
MONGODB_* / CLICKHOUSE_* / NEO4J_* |
live contract per document/columnar/graph engine |
tests/test_mysql_integration.py |
MYSQL_* or MARIADB_* |
live MySQL/MariaDB contract, incl. mariadb fallback to MYSQL_* |
tests/test_postgres_integration.py |
POSTGRES_DSN |
schema, idempotent upserts, JSONB/array round-trip, no-clobber, coverage, edge counts |
tests/test_surrealdb_integration.py |
SURREAL_* |
schema, idempotent upserts, document payload, coverage, graph edges |
tests/test_dual_write_integration.py |
two or more sinks | the same filing lands in every configured sink; document status mirrors |
Fixtures, fault injection, and the canary¶
- Replayed API contract (
tests/test_api_contract.py):responsesserves the recorded response shapes (JSF page, form POST, JSON pages), so the full fetch path — ViewState extraction, pagination, tolerant parsing of nulls/non-strings/CJK titles — is exercised offline. Regressions here mean the HKEx API changed shape. - Fault injection (
tests/test_fault_injection.py): scripted sinks prove that one sink failing (or raising) never blocks another, that failures are counted, and that the run exits non-zero. - Canary (
scripts/canary.py,.github/workflows/canary.yml): a daily live check that fetches a small recent window and asserts the shape is unchanged, opening an issue when it is not. Run it by hand withpython scripts/canary.py --days 7 --max 5 --json.
CI¶
.github/workflows/ci.yml runs lint/format, a markdown lint, unit tests on Python 3.10–3.13
(with DuckDB, so its in-process contract tests run), and integration jobs: PostgreSQL (service
container), MySQL (service container), SurrealDB (docker run), multi-sink (PostgreSQL +
SurrealDB), and a job covering MongoDB, ClickHouse, and Neo4j service containers. The SurrealDB image
is pinned to v3.2.4.