test: structurally-exact raises shape and identifier-normalized table matching
- raises block: func must be Attribute pytest.raises or a bare Name 'raises'; matcher must be a Name/Attribute chain whose final segment is exactly IntegrityError (fake.IntegrityError/NotIntegrityError fail) - insert target matching tokenizes the leading identifier, strips quotes/brackets, and takes the last dot-segment, so INSERT OR IGNORE INTO "main"."children" and attached-qualified spellings all count as table inserts requiring the raises block
This commit is contained in:
+50
-37
@@ -654,53 +654,67 @@ def test_dao_matrix_test_file_raw_probes_are_only_constraint_probes() -> None:
|
|||||||
raw_statements.append((sub, _unquote(rendered)))
|
raw_statements.append((sub, _unquote(rendered)))
|
||||||
|
|
||||||
def _raises_blocks():
|
def _raises_blocks():
|
||||||
"""Yield (block, matcher_name, func_ok) for pytest.raises."""
|
"""Yield (block, is_pytest_integrity) for pytest.raises blocks.
|
||||||
|
|
||||||
|
Structurally exact: the call func must resolve to
|
||||||
|
pytest.raises (Attribute pytest.raises, or a bare Name
|
||||||
|
imported from pytest — checked against the file's import
|
||||||
|
section), and the matcher argument must be an AST chain
|
||||||
|
whose final segment is exactly IntegrityError (so
|
||||||
|
NotIntegrityError and fake.IntegrityError fail).
|
||||||
|
"""
|
||||||
for sub in ast.walk(node):
|
for sub in ast.walk(node):
|
||||||
if not (
|
if not (
|
||||||
isinstance(sub, ast.With)
|
isinstance(sub, ast.With)
|
||||||
and sub.items
|
and sub.items
|
||||||
and isinstance(sub.items[0].context_expr, ast.Call)
|
and isinstance(sub.items[0].context_expr, ast.Call)
|
||||||
and isinstance(
|
|
||||||
sub.items[0].context_expr.func, ast.Attribute
|
|
||||||
)
|
|
||||||
and sub.items[0].context_expr.func.attr == "raises"
|
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
call = sub.items[0].context_expr
|
call = sub.items[0].context_expr
|
||||||
# func must be pytest.raises: Attribute pytest.raises or
|
func = call.func
|
||||||
# bare `raises` imported from pytest with module prefix
|
if isinstance(func, ast.Attribute):
|
||||||
# check via source (this file imports pytest).
|
if (
|
||||||
func_src = ast.unparse(call.func)
|
func.attr != "raises"
|
||||||
matcher = ""
|
or not isinstance(func.value, ast.Name)
|
||||||
|
or func.value.id != "pytest"
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
elif isinstance(func, ast.Name):
|
||||||
|
if func.id != "raises":
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
matcher_ok = False
|
||||||
if call.args:
|
if call.args:
|
||||||
matcher = ast.unparse(call.args[0])
|
matcher = call.args[0]
|
||||||
yield sub, matcher, func_src.endswith("raises")
|
# Accept a Name IntegrityError or an Attribute chain
|
||||||
|
# ending in the exact Name IntegrityError.
|
||||||
|
if isinstance(matcher, ast.Name):
|
||||||
|
matcher_ok = matcher.id == "IntegrityError"
|
||||||
|
elif isinstance(matcher, ast.Attribute):
|
||||||
|
matcher_ok = matcher.attr == "IntegrityError"
|
||||||
|
yield sub, matcher_ok
|
||||||
|
|
||||||
def _is_integrity_raises(matcher: str) -> bool:
|
# INSERT OR variant / quoted / qualified / schema-qualified
|
||||||
"""Exact AST match: the exception argument names IntegrityError."""
|
# names: normalize the SQL so ANY spelling of an insert into
|
||||||
# unparse of sqlite3.IntegrityError / IntegrityError /
|
# the table counts as a table insert and requires the raises
|
||||||
# sqlite3.errors.IntegrityError all END with IntegrityError
|
# block. Strip quotes/brackets/backticks from identifiers and
|
||||||
# as a complete dotted name; NotIntegrityError would end
|
# drop any schema qualifier, whatever it is.
|
||||||
# with it too, so require the char before to be non-word.
|
|
||||||
return re.search(
|
|
||||||
r"(?:^|[^A-Za-z0-9_])IntegrityError$", matcher
|
|
||||||
) is not None
|
|
||||||
|
|
||||||
# INSERT OR variant / quoted / qualified names: normalize the
|
|
||||||
# SQL before matching so INSERT OR IGNORE INTO children,
|
|
||||||
# INSERT INTO [children], INSERT INTO "children" and
|
|
||||||
# INSERT INTO main.children all count as table inserts.
|
|
||||||
def _is_insert_into_table(rendered: str, table: str) -> bool:
|
def _is_insert_into_table(rendered: str, table: str) -> bool:
|
||||||
normalized = re.sub(
|
insert_match = re.search(
|
||||||
r"INSERT(\s+OR\s+\w+)?\s+INTO\s+"
|
r"\bINSERT(\s+OR\s+\w+)?\s+INTO\s+(.+)",
|
||||||
r"((main|temp)\s*\.\s*)?[`'\"]*(\[)?"
|
|
||||||
+ table
|
|
||||||
+ r"\b",
|
|
||||||
"TABLE_MATCH",
|
|
||||||
rendered,
|
rendered,
|
||||||
flags=re.IGNORECASE,
|
flags=re.IGNORECASE | re.DOTALL,
|
||||||
)
|
)
|
||||||
return "TABLE_MATCH" in normalized
|
if not insert_match:
|
||||||
|
return False
|
||||||
|
target = insert_match.group(2)
|
||||||
|
# Tokenize the target's leading identifier: strip quotes
|
||||||
|
# and brackets, take the LAST dot-separated segment.
|
||||||
|
identifier = re.split(r"[\s(]", target, maxsplit=1)[0]
|
||||||
|
segments = re.split(r"\.", identifier)
|
||||||
|
last = segments[-1].strip("`'\"[]")
|
||||||
|
return last == table
|
||||||
|
|
||||||
for insert, rendered in raw_statements:
|
for insert, rendered in raw_statements:
|
||||||
if not any_table.search(rendered):
|
if not any_table.search(rendered):
|
||||||
@@ -715,9 +729,8 @@ def test_dao_matrix_test_file_raw_probes_are_only_constraint_probes() -> None:
|
|||||||
inside = any(
|
inside = any(
|
||||||
block.lineno <= insert.lineno
|
block.lineno <= insert.lineno
|
||||||
and insert.end_lineno <= block.end_lineno
|
and insert.end_lineno <= block.end_lineno
|
||||||
and _is_integrity_raises(matcher)
|
and matcher_ok
|
||||||
and func_ok
|
for block, matcher_ok in _raises_blocks()
|
||||||
for block, matcher, func_ok in _raises_blocks()
|
|
||||||
)
|
)
|
||||||
assert inside, (
|
assert inside, (
|
||||||
f"the raw {table} insert in {node.name} must be nested "
|
f"the raw {table} insert in {node.name} must be nested "
|
||||||
|
|||||||
Reference in New Issue
Block a user