Skip to content

Commit 17640a3

Browse files
feat(tests): add local integration test suite for proxy and plugins
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e19fab0 commit 17640a3

8 files changed

Lines changed: 515 additions & 47 deletions

File tree

Makefile

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
SHELL := /bin/bash
12
VENV_DIR ?= .venv
23
VENV_RUN = . $(VENV_DIR)/bin/activate
34
PIP_CMD ?= pip
5+
PYTHON_CMD ?= python
6+
TEST_DEPS ?= pytest pytest-timeout
7+
LINT_DEPS ?= ruff
8+
9+
PG_TEST_CONTAINER ?= pg-proxy-local-tests
10+
PG_TEST_IMAGE ?= postgres:16
11+
PG_TEST_PORT ?= 55432
12+
PG_TEST_USER ?= postgres
13+
PG_TEST_PASSWORD ?= postgres
14+
PG_TEST_DB ?= postgres
415

516
usage: ## Show this help
617
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
@@ -14,4 +25,42 @@ install: ## Install dependencies in local virtualenv folder
1425
publish: ## Publish the library to the central PyPi repository
1526
($(VENV_RUN); pip install twine; python ./setup.py sdist && twine upload dist/*)
1627

17-
.PHONY: usage install clean publish test lint
28+
install-test: install ## Install test dependencies in local virtualenv
29+
($(VENV_RUN); $(PIP_CMD) install $(TEST_DEPS))
30+
31+
install-lint: install ## Install lint dependencies in local virtualenv
32+
($(VENV_RUN); $(PIP_CMD) install $(LINT_DEPS))
33+
34+
lint: install-lint ## Format code with ruff
35+
$(VENV_DIR)/bin/ruff format postgresql_proxy tests plugins
36+
37+
test: ## Start local PostgreSQL container and run all tests
38+
@set -euo pipefail; \
39+
cleanup() { docker rm -f $(PG_TEST_CONTAINER) >/dev/null 2>&1 || true; }; \
40+
trap cleanup EXIT INT TERM; \
41+
docker rm -f $(PG_TEST_CONTAINER) >/dev/null 2>&1 || true; \
42+
docker run --name $(PG_TEST_CONTAINER) \
43+
-e POSTGRES_USER=$(PG_TEST_USER) \
44+
-e POSTGRES_PASSWORD=$(PG_TEST_PASSWORD) \
45+
-e POSTGRES_DB=$(PG_TEST_DB) \
46+
-p $(PG_TEST_PORT):5432 \
47+
-d $(PG_TEST_IMAGE) >/dev/null; \
48+
for i in $$(seq 1 45); do \
49+
if docker exec $(PG_TEST_CONTAINER) pg_isready -U $(PG_TEST_USER) >/dev/null 2>&1; then \
50+
echo "PostgreSQL ready on 127.0.0.1:$(PG_TEST_PORT)"; \
51+
break; \
52+
fi; \
53+
sleep 1; \
54+
done; \
55+
if ! docker exec $(PG_TEST_CONTAINER) pg_isready -U $(PG_TEST_USER) >/dev/null 2>&1; then \
56+
echo "PostgreSQL did not become ready in time"; \
57+
exit 1; \
58+
fi; \
59+
E2E_PG_HOST=127.0.0.1 \
60+
E2E_PG_PORT=$(PG_TEST_PORT) \
61+
E2E_PG_USER=$(PG_TEST_USER) \
62+
E2E_PG_PASSWORD=$(PG_TEST_PASSWORD) \
63+
E2E_PG_DB=$(PG_TEST_DB) \
64+
$(VENV_DIR)/bin/$(PYTHON_CMD) -m pytest -vv
65+
66+
.PHONY: usage install install-test install-lint clean publish test lint

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,11 @@ If you want to test it, do this. Otherwise scroll down for instructions on how t
8181
- add stop() method to proxy; refactor logging
8282
- v0.0.2
8383
- fix socket file descriptors under Linux
84+
85+
## Testing
86+
87+
Run the full local test suite (starts a disposable PostgreSQL container automatically):
88+
89+
```bash
90+
make test
91+
```

plugins/tableau_hll/test.py

Lines changed: 0 additions & 34 deletions
This file was deleted.

tests/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Testing Guide
2+
3+
All tests in this repo require a real PostgreSQL server and are organized at the top level:
4+
5+
- `test_proxy.py`: proxy behavior tests (connection, SSL, hang regressions)
6+
- `test_plugins.py`: plugin integration tests (HLL rewrite behavior)
7+
8+
## Prerequisites
9+
10+
- Python `3.13` (same version as CI)
11+
- Docker (for local disposable PostgreSQL)
12+
- `psql` (`postgresql-client`)
13+
- `openssl` (SSL tests generate a temporary self-signed cert/key at runtime)
14+
15+
Install Python deps in the project virtualenv:
16+
17+
```bash
18+
make install-test
19+
```
20+
21+
## Which command should I use?
22+
23+
- Fastest full local run with disposable Postgres: `make test`
24+
- Run only proxy tests (using your own Postgres): `python -m pytest tests/test_proxy.py -vv`
25+
- Run only plugin tests: `python -m pytest tests/test_plugins.py -vv`
26+
27+
## 1) Full local suite (recommended)
28+
29+
`make test` starts a temporary PostgreSQL container, waits for readiness, sets DB env vars, then runs:
30+
31+
```bash
32+
python -m pytest -vv
33+
```
34+
35+
Use it when you want one command that matches normal contributor workflow.
36+
37+
```bash
38+
make test
39+
```
40+
41+
## 2) DB-backed proxy tests against an existing PostgreSQL
42+
43+
If you already have PostgreSQL running, set connection env vars and run only proxy tests:
44+
45+
```bash
46+
export E2E_PG_HOST=127.0.0.1
47+
export E2E_PG_PORT=5432
48+
export E2E_PG_USER=postgres
49+
export E2E_PG_PASSWORD=postgres
50+
export E2E_PG_DB=postgres
51+
python -m pytest tests/test_proxy.py -vv
52+
```
53+
54+
If PostgreSQL is not reachable, tests fail fast at startup.
55+
56+
## 3) Plugin integration tests
57+
58+
```bash
59+
python -m pytest tests/test_plugins.py -vv
60+
```
61+
62+
Requires PostgreSQL to be running with the `E2E_PG_*` env vars set (see section 2).

tests/conftest.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import os
2+
3+
import psycopg2
4+
import pytest
5+
6+
7+
@pytest.fixture(scope="session")
8+
def postgres_settings():
9+
"""PostgreSQL connection settings from environment or defaults."""
10+
return {
11+
"host": os.environ.get("E2E_PG_HOST", "127.0.0.1"),
12+
"port": int(os.environ.get("E2E_PG_PORT", "5432")),
13+
"user": os.environ.get("E2E_PG_USER", "postgres"),
14+
"password": os.environ.get("E2E_PG_PASSWORD", "postgres"),
15+
"dbname": os.environ.get("E2E_PG_DB", "postgres"),
16+
}
17+
18+
19+
@pytest.fixture(scope="session", autouse=True)
20+
def ensure_postgres_available(postgres_settings):
21+
"""Ensure PostgreSQL backend is available before running any tests."""
22+
try:
23+
with psycopg2.connect(
24+
connect_timeout=3, sslmode="disable", **postgres_settings
25+
) as conn:
26+
with conn.cursor() as cur:
27+
cur.execute("SELECT 1")
28+
assert cur.fetchone() == (1,)
29+
except Exception as err: # pragma: no cover - environment dependent
30+
pytest.fail(
31+
f"PostgreSQL backend is required for tests but is not reachable: {err}"
32+
)
33+

tests/test_plugin.py

Lines changed: 0 additions & 12 deletions
This file was deleted.

tests/test_plugins.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Plugin integration tests.
2+
3+
These tests verify we can load plugins and that plugin behavior works against a real Postgres backend.
4+
"""
5+
6+
import collections
7+
import importlib
8+
9+
import psycopg2
10+
import pytest
11+
12+
import plugins.tableau_hll as hll
13+
14+
15+
@pytest.fixture()
16+
def plugin_context(postgres_settings, monkeypatch):
17+
# plugin's internal psycopg2 connection does not pass password, so provide it via libpq env var
18+
monkeypatch.setenv("PGPASSWORD", postgres_settings["password"])
19+
20+
with psycopg2.connect(sslmode="disable", **postgres_settings) as conn:
21+
conn.autocommit = True
22+
with conn.cursor() as cur:
23+
cur.execute('CREATE SCHEMA IF NOT EXISTS "crm_dim";')
24+
cur.execute('DROP TABLE IF EXISTS "crm_dim"."crm_data_source";')
25+
cur.execute(
26+
"""
27+
DO $$
28+
BEGIN
29+
IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'hll' AND typtype = 'd') THEN
30+
DROP DOMAIN hll;
31+
END IF;
32+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'hll') THEN
33+
CREATE TYPE hll AS (v text);
34+
END IF;
35+
END $$;
36+
"""
37+
)
38+
cur.execute(
39+
'CREATE TABLE "crm_dim"."crm_data_source" ('
40+
'"Set of Customers" hll, '
41+
'"Campaign Name" text);'
42+
)
43+
44+
InstanceConfig = collections.namedtuple("InstanceConfig", "redirect")
45+
Redirect = collections.namedtuple("Redirect", "name host port")
46+
return {
47+
"instance_config": InstanceConfig(
48+
redirect=Redirect(
49+
name="postgres",
50+
host=postgres_settings["host"],
51+
port=postgres_settings["port"],
52+
)
53+
),
54+
"connect_params": {
55+
"user": postgres_settings["user"],
56+
"database": postgres_settings["dbname"],
57+
},
58+
}
59+
60+
61+
def test_rewrite_query_for_hll_column(plugin_context):
62+
src = (
63+
'SELECT COUNT(DISTINCT "crm_data_source"."Set of Customers") AS "ctd:Set of Customers:ok"\n'
64+
'FROM "crm_dim"."crm_data_source" "crm_data_source"\n'
65+
"HAVING (COUNT(1) > 0);"
66+
)
67+
68+
res = hll.rewrite_query(src, plugin_context)
69+
assert "hll_cardinality(hll_union_agg" in res
70+
71+
72+
def test_plugin_module_loads_and_exposes_rewriter():
73+
module = importlib.import_module("plugins.tableau_hll")
74+
assert hasattr(module, "rewrite_query")
75+
assert callable(module.rewrite_query)
76+
77+
78+
def test_does_not_rewrite_non_hll_column(plugin_context):
79+
src = (
80+
'SELECT COUNT(DISTINCT "crm_data_source"."Campaign Name") AS "ctd:Campaign Name:ok"\n'
81+
'FROM "crm_dim"."crm_data_source" "crm_data_source"\n'
82+
"HAVING (COUNT(1) > 0);"
83+
)
84+
85+
res = hll.rewrite_query(src, plugin_context)
86+
assert "hll_cardinality(hll_union_agg" not in res

0 commit comments

Comments
 (0)