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
2 changes: 2 additions & 0 deletions .actrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--container-architecture linux/arm64
-P ubuntu-24.04=catthehacker/ubuntu:act-latest
54 changes: 54 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Run Tests

on:
pull_request:
push:
branches:
- master

jobs:
tests:
runs-on: ubuntu-24.04

services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5

env:
E2E_PG_HOST: 127.0.0.1
E2E_PG_PORT: 5432
E2E_PG_USER: postgres
E2E_PG_PASSWORD: postgres
E2E_PG_DB: postgres

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y postgresql-client
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-timeout

- name: Run tests
run: |
python -m pytest -vv

60 changes: 58 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,17 +1,73 @@
VENV_DIR ?= .venv
VENV_RUN = . $(VENV_DIR)/bin/activate
PIP_CMD ?= pip
PYTHON_CMD ?= python
TEST_DEPS ?= pytest pytest-timeout
LINT_DEPS ?= ruff

PG_TEST_CONTAINER ?= pg-proxy-local-tests
PG_TEST_IMAGE ?= postgres:16
PG_TEST_PORT ?= 55432
PG_TEST_USER ?= postgres
PG_TEST_PASSWORD ?= postgres
PG_TEST_DB ?= postgres
ACT_CMD ?= act
ACT_WORKFLOW ?= .github/workflows/tests.yml
ACT_JOB ?= tests
ACT_PULL ?= false
ACT_CONTAINER_ARCH ?= linux/arm64

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

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

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

.PHONY: usage install clean publish test lint
install-test: install ## Install test dependencies in local virtualenv
($(VENV_RUN); $(PIP_CMD) install $(TEST_DEPS))

install-lint: install ## Install lint dependencies in local virtualenv
($(VENV_RUN); $(PIP_CMD) install $(LINT_DEPS))

lint: install-lint ## Format code with ruff
$(VENV_DIR)/bin/ruff format postgresql_proxy tests plugins

test: ## Start local PostgreSQL container and run all tests
@set -euo pipefail; \
cleanup() { docker rm -f $(PG_TEST_CONTAINER) >/dev/null 2>&1 || true; }; \
trap cleanup EXIT INT TERM; \
docker rm -f $(PG_TEST_CONTAINER) >/dev/null 2>&1 || true; \
docker run --name $(PG_TEST_CONTAINER) \
-e POSTGRES_USER=$(PG_TEST_USER) \
-e POSTGRES_PASSWORD=$(PG_TEST_PASSWORD) \
-e POSTGRES_DB=$(PG_TEST_DB) \
-p $(PG_TEST_PORT):5432 \
-d $(PG_TEST_IMAGE) >/dev/null; \
for i in $$(seq 1 45); do \
if docker exec $(PG_TEST_CONTAINER) pg_isready -U $(PG_TEST_USER) >/dev/null 2>&1; then \
echo "PostgreSQL ready on 127.0.0.1:$(PG_TEST_PORT)"; \
break; \
fi; \
sleep 1; \
done; \
if ! docker exec $(PG_TEST_CONTAINER) pg_isready -U $(PG_TEST_USER) >/dev/null 2>&1; then \
echo "PostgreSQL did not become ready in time"; \
exit 1; \
fi; \
E2E_PG_HOST=127.0.0.1 \
E2E_PG_PORT=$(PG_TEST_PORT) \
E2E_PG_USER=$(PG_TEST_USER) \
E2E_PG_PASSWORD=$(PG_TEST_PASSWORD) \
E2E_PG_DB=$(PG_TEST_DB) \
$(VENV_DIR)/bin/$(PYTHON_CMD) -m pytest -vv

test-act: ## Run the CI test workflow locally with act
$(ACT_CMD) -W $(ACT_WORKFLOW) -j $(ACT_JOB) --pull=$(ACT_PULL) --container-architecture $(ACT_CONTAINER_ARCH)

.PHONY: usage install install-test install-lint clean publish test test-act lint
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,38 @@ If you want to test it, do this. Otherwise scroll down for instructions on how t
- add stop() method to proxy; refactor logging
- v0.0.2
- fix socket file descriptors under Linux



## Testing

CI runs tests on Python `3.13`, so use Python `3.13` locally for parity.

Run the full local test suite (starts a disposable PostgreSQL container automatically):

```bash
make test
```

Run the GitHub Actions test workflow locally with [`act`](https://github.com/nektos/act):

On macOS, install `act` with Homebrew:

```bash
brew install act
```

```bash
make test-act
```

Useful overrides for local runs:

```bash
# Refresh images explicitly when needed
make test-act ACT_PULL=true

# Match GitHub runner architecture on Apple Silicon (slower)
make test-act ACT_CONTAINER_ARCH=linux/amd64
```

34 changes: 0 additions & 34 deletions plugins/tableau_hll/test.py

This file was deleted.

12 changes: 12 additions & 0 deletions postgresql_proxy/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ def accept_wrapper(self, sock: socket.socket):

# Accept the raw connection
clientsocket, address = sock.accept()
# On macOS, accepted sockets inherit O_NONBLOCK from the listening socket.
# SSL negotiation uses blocking recv, so we must set blocking explicitly here.
clientsocket.setblocking(True)

# Check if SSL is enabled for this proxy
if self.ssl_context:
Expand Down Expand Up @@ -234,6 +237,15 @@ def service_connection(self, key: SelectorKeyProxy, mask):
if recv_data:
LOG.debug('%s received data:\n%s', conn.name, recv_data)
conn.received(recv_data)
# excerpt from https://docs.python.org/3/library/ssl.html#ssl-nonblocking
# Conversely, since the SSL layer has its own framing, a SSL socket may still have data available
# for reading without select() being aware of it. Therefore, you should first call SSLSocket.recv()
# to drain any potentially available data, and then only block on a select() call if still necessary.
while isinstance(sock, ssl.SSLSocket) and sock.pending() > 0:
extra = sock.recv(min(4096, sock.pending()))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I know this is from the code I shared last week, but now that I read it, I don't think the min comparison is really necessary since the recv method already works that it grabs what is present until value provided in arg. We can probably safely achieve the same result with extra = sock.recv(4096).

if extra:
LOG.debug('%s received pending SSL data:\n%s', conn.name, extra)
conn.received(extra)
else:
self._unregister_conn(conn)
LOG.debug('%s connection closing %s', conn.name, conn.address)
Expand Down
80 changes: 80 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Testing Guide

All tests in this repo require a real PostgreSQL server and are organized at the top level:

- `test_proxy.py`: proxy behavior tests (connection, SSL, hang regressions)
- `test_plugins.py`: plugin integration tests (HLL rewrite behavior)

## Prerequisites

- Python `3.13` (same version as CI)
- Docker (for local disposable PostgreSQL)
- `psql` (`postgresql-client`)
- `openssl` (SSL tests generate a temporary self-signed cert/key at runtime)
- `act` (optional, for local GitHub Actions runs)

Install Python deps in the project virtualenv:

```bash
make install-test
```

## Which command should I use?

- Fastest full local run with disposable Postgres: `make test`
- Run only proxy tests (using your own Postgres): `python -m pytest tests/test_proxy.py -vv`
- Run only plugin tests: `python -m pytest tests/test_plugins.py -vv`
- Run exact CI workflow locally: `make test-act`

## 1) Full local suite (recommended)

`make test` starts a temporary PostgreSQL container, waits for readiness, sets DB env vars, then runs:

```bash
python -m pytest -vv
```

Use it when you want one command that matches normal contributor workflow.

```bash
make test
```

## 2) DB-backed proxy tests against an existing PostgreSQL

If you already have PostgreSQL running, set connection env vars and run only proxy tests:

```bash
export E2E_PG_HOST=127.0.0.1
export E2E_PG_PORT=5432
export E2E_PG_USER=postgres
export E2E_PG_PASSWORD=postgres
export E2E_PG_DB=postgres
python -m pytest tests/test_proxy.py -vv
```

If PostgreSQL is not reachable, tests fail fast at startup.

## 3) Plugin integration tests

```bash
python -m pytest tests/test_plugins.py -vv
```

Requires PostgreSQL to be running with the `E2E_PG_*` env vars set (see section 2).

## 4) Run the GitHub workflow locally (`act`)

```bash
make test-act
```

Useful overrides:

```bash
# Refresh workflow images
make test-act ACT_PULL=true

# Match GitHub x86_64 runner architecture (slower on Apple Silicon)
make test-act ACT_CONTAINER_ARCH=linux/amd64
```
33 changes: 33 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os

import psycopg2
import pytest


@pytest.fixture(scope="session")
def postgres_settings():
"""PostgreSQL connection settings from environment or defaults."""
return {
"host": os.environ.get("E2E_PG_HOST", "127.0.0.1"),
"port": int(os.environ.get("E2E_PG_PORT", "5432")),
"user": os.environ.get("E2E_PG_USER", "postgres"),
"password": os.environ.get("E2E_PG_PASSWORD", "postgres"),
"dbname": os.environ.get("E2E_PG_DB", "postgres"),
}


@pytest.fixture(scope="session", autouse=True)
def ensure_postgres_available(postgres_settings):
"""Ensure PostgreSQL backend is available before running any tests."""
try:
with psycopg2.connect(
connect_timeout=3, sslmode="disable", **postgres_settings
) as conn:
with conn.cursor() as cur:
cur.execute("SELECT 1")
assert cur.fetchone() == (1,)
except Exception as err: # pragma: no cover - environment dependent
pytest.fail(
f"PostgreSQL backend is required for tests but is not reachable: {err}"
)

12 changes: 0 additions & 12 deletions tests/test_plugin.py

This file was deleted.

Loading
Loading