feat: add file pytest test-file emitter#5
Conversation
matte97p
left a comment
There was a problem hiding this comment.
Hey @codeneu-A15, thanks for picking this up! The overall shape matches the pgtap reference and the CLI wiring is clean. Before we can merge, there are a few issues to work through — some are structural, so I'd rather flag them all up front than drip-feed review rounds.
Blockers (the emitted file won't run as-is)
-
Claim setter produces invalid Python source.
json.dumps(rendered)contains", then gets embedded inside an outerf"db_client.execute(\"…'{json_str}'…\")"— the inner"closes the outer string. The generated test file won't parse. Same class of bug in the cross-tenantWHEREbuilder:f"\"{col}\" = \\'{val}\\'"produces"id" = \'…\'which breaks both the surrounding Python string and is invalid SQL (Postgres doesn't honor\'outside E-strings).
→ Fix: stop building SQL as interpolated strings inside emitted Python. Emit parameterized calls:db_client.execute("SELECT count(*) FROM {qualified} WHERE \"id\" = %s", (pk_val,)). Removes a whole class of escaping bugs. -
SET LOCAL ROLE '{cell.role}'is wrong syntax.SET ROLEexpects an identifier (SET ROLE fooorSET ROLE "foo"), not a string literal. Postgres errors out. Use the same_quote_identpattern pgtap uses. -
SET LOCALoutside a transaction is a no-op + warning. pgtap wraps everything inBEGIN; … ROLLBACK;. The pytest emitter delegates to thedb_clientfixture but doesn't document that it must be transactional per-test. Either require it explicitly in the fixture docstring, or useSET ROLE(session) +RESET ROLEin atry/finally. -
Identifier quoting is missing everywhere.
qualified = f"{cell.schema}.{cell.table}"raw. Tables with mixed case / reserved words / dots break. Reuse_quote_qualifiedfrom pgtap.
Strong suggestions
-
Extract shared helpers.
_quote_ident,_quote_literal,_quote_qualified,_is_bypass_role,_BYPASS_ROLES,_conditional_cells_with_datashould move toemitters/_common.pyso both emitters import them. Right now you're importing a private_conditional_cells_with_datacross-module. -
Bypass roles aren't filtered on the base path. pgtap skips
service_role/postgres/supabase_admin. The pytest emitter only filters them inside_conditional_cells_with_data. Base ALLOW/DENY tests for those roles will fail spuriously. -
UPDATE probe hardcodes
id.UPDATE {qualified} SET id = id WHERE false— tables without anidcolumn return42703 column does not exist, not42501. Useintrospection.pk_of(schema, table)like pgtap does. -
pytest.raises(Exception, match='42501')is fragile. psycopg3 carries SQLSTATE onexc.diag.sqlstate, not always instr(exc). PlusExceptionis too broad — aKeyErrorfrom the fixture would pass. Prefer:with pytest.raises(Exception) as exc_info: db_client.execute(stmt) assert getattr(exc_info.value, 'sqlstate', None) == '42501'
Or catch
psycopg.errors.InsufficientPrivilegedirectly if you want to pin the driver. -
RESET ROLEruns after the assertion. Ifassert count == 0fails, the exception skipsRESET ROLEand the role leaks into the next test sharing the connection. Wrap intry/finally, or rely on a transactional fixture that rolls back per-test. -
Add
tests/test_pytest_emit.py. Mirrortest_pgtap_emit.py— at minimum a golden test for the base path and one with aseed_statefixture covering the conditional path. That second one would have caught blocker #1.
Nits
import json as _jsoninsidegen_pytest— move to module top, like the rest ofcli.py.# Simplified for templatecomment on the UPDATE probe → resolve viapk_of()(#7) and drop the marker.- Test function names use
{role}_{op}_{table}— two schemas with the same table name produce duplicatedef test_…names. Include the schema (sanitized) or use a hash suffix.
Happy to pair on any of this, especially #1 (the parameterized rewrite) and #5 (the helper extraction) since those are the structural calls. Once those two land the rest is mechanical.
Closes #3
Hi @matte97p! Here is the
pytesttest-file emitter for this issue.I have implemented the following things:
src/rlsgrid/emitters/pytest.pybased on the pgTAP reference._lives,_throws, and_is_count_zeroassertions.gen pytestCLI command.I tested it locally against the dummy blog database and it successfully generates the
test_rls.pyfile with all the assertions passing.