""" Tests for the check-utcnow-parens pre-commit hook. Verifies that the hook flags default=utcnow and onupdate=utcnow (without parentheses) while allowing the correct default=utcnow() and server_default=utcnow() patterns. """ import subprocess import sys import tempfile from pathlib import Path HOOK_SCRIPT = ( Path(__file__).parent.parent.parent / ".pre-commit-hooks" / "check-utcnow-parens.py" ) def _run_hook(content: str) -> subprocess.CompletedProcess: """Write content to a temp .py file and run the hook against it.""" with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as f: f.write(content) f.flush() return subprocess.run( [sys.executable, str(HOOK_SCRIPT), f.name], capture_output=True, text=True, ) # ========================================================================= # Should flag (exit code 1) # ========================================================================= class TestFlagsIncorrectPatterns: """Patterns that must be caught by the hook.""" def test_default_without_parens(self): result = _run_hook( "created_at = Column(UtcDateTime, default=utcnow, nullable=False)\n" ) assert result.returncode == 1 assert "utcnow without parentheses" in result.stdout def test_onupdate_without_parens(self): result = _run_hook( "updated_at = Column(UtcDateTime, onupdate=utcnow, nullable=False)\n" ) assert result.returncode == 1 assert "utcnow without parentheses" in result.stdout def test_both_default_and_onupdate_without_parens(self): result = _run_hook( "updated_at = Column(UtcDateTime, default=utcnow, onupdate=utcnow, nullable=False)\n" ) assert result.returncode == 1 assert "utcnow without parentheses" in result.stdout def test_default_without_parens_trailing_paren(self): """default=utcnow) — missing opening paren.""" result = _run_hook("created_at = Column(UtcDateTime, default=utcnow)\n") assert result.returncode == 1 def test_default_without_parens_multiline(self): result = _run_hook( "updated_at = Column(\n" " UtcDateTime, default=utcnow, onupdate=utcnow, nullable=False\n" ")\n" ) assert result.returncode == 1 def test_default_with_spaces_around_equals(self): result = _run_hook( "created_at = Column(UtcDateTime, default = utcnow, nullable=False)\n" ) assert result.returncode == 1 # ========================================================================= # Should pass (exit code 0) # ========================================================================= class TestAllowsCorrectPatterns: """Patterns that must NOT be flagged.""" def test_default_with_parens(self): result = _run_hook( "created_at = Column(UtcDateTime, default=utcnow(), nullable=False)\n" ) assert result.returncode == 0 def test_onupdate_with_parens(self): result = _run_hook( "updated_at = Column(UtcDateTime, onupdate=utcnow(), nullable=False)\n" ) assert result.returncode == 0 def test_server_default_with_parens(self): result = _run_hook( "created_at = Column(UtcDateTime, server_default=utcnow(), nullable=False)\n" ) assert result.returncode == 0 def test_both_with_parens(self): result = _run_hook( "updated_at = Column(UtcDateTime, default=utcnow(), onupdate=utcnow(), nullable=False)\n" ) assert result.returncode == 0 def test_no_utcnow_at_all(self): result = _run_hook("name = Column(String(100), nullable=False)\n") assert result.returncode == 0 def test_utcnow_in_comment(self): """Comments mentioning utcnow() should not be flagged.""" result = _run_hook( "# Note: created_at uses default=utcnow() in the model\n" ) assert result.returncode == 0 def test_utcnow_import(self): result = _run_hook("from sqlalchemy_utc import UtcDateTime, utcnow\n") assert result.returncode == 0 def test_utcnow_assignment(self): """Direct assignment like existing_rating.created_at = utcnow().""" result = _run_hook("existing_rating.created_at = utcnow()\n") assert result.returncode == 0 def test_empty_file(self): result = _run_hook("") assert result.returncode == 0 # ========================================================================= # Comment / docstring skip (regression) # ========================================================================= class TestCommentAndDocstringSkip: """Lines that start with a comment or docstring delimiter must not fire, even if they contain the literal bad pattern.""" def test_comment_with_bare_utcnow_not_flagged(self): """Key regression: `# default=utcnow is wrong` must not fire.""" result = _run_hook("# default=utcnow is wrong — use default=utcnow()\n") assert result.returncode == 0 def test_comment_with_onupdate_bare_utcnow_not_flagged(self): result = _run_hook("# onupdate=utcnow was the old style\n") assert result.returncode == 0 def test_triple_double_quote_line_not_flagged(self): result = _run_hook('"""default=utcnow was the wrong style."""\n') assert result.returncode == 0 def test_triple_single_quote_line_not_flagged(self): result = _run_hook("'''default=utcnow was the wrong style.'''\n") assert result.returncode == 0 def test_indented_comment_not_flagged(self): """Indented comments also respect the skip.""" result = _run_hook(" # default=utcnow\n") assert result.returncode == 0 # ========================================================================= # Output format # ========================================================================= class TestOutputFormat: """Verify the error messages include useful context.""" def test_shows_filename_and_line(self): result = _run_hook( "x = 1\n" "created_at = Column(UtcDateTime, default=utcnow, nullable=False)\n" ) assert result.returncode == 1 assert ":2:" in result.stdout # Line 2 def test_shows_hint(self): result = _run_hook( "created_at = Column(UtcDateTime, default=utcnow, nullable=False)\n" ) assert "Hint:" in result.stdout assert "SQL expression" in result.stdout