Skip to content

Commit 1c4344d

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 1c4344d

8 files changed

Lines changed: 515 additions & 48 deletions

File tree

Makefile

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

515
usage: ## Show this help
616
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
717

818
install: ## Install dependencies in local virtualenv folder
9-
(test `which virtualenv` || $(PIP_CMD) install --user virtualenv) && \
19+
(test `which virtualenv` || $(PIP_CMD) install virtualenv) && \
1020
(test -e $(VENV_DIR) || virtualenv $(VENV_OPTS) $(VENV_DIR)) && \
1121
($(VENV_RUN) && $(PIP_CMD) install --upgrade pip) && \
1222
(test ! -e requirements.txt || ($(VENV_RUN); $(PIP_CMD) install -r requirements.txt))
1323

1424
publish: ## Publish the library to the central PyPi repository
1525
($(VENV_RUN); pip install twine; python ./setup.py sdist && twine upload dist/*)
1626

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