Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

307 lines
11 KiB
Python

"""
Tests for the check-datetime-timezone pre-commit hook.
Verifies that the hook flags any ``Column(DateTime(...))`` /
``sa.Column(..., DateTime(...))`` pattern in both model files under
``src/local_deep_research/database/models/`` and migration files under
``src/local_deep_research/database/migrations/versions/``, and allows
``UtcDateTime`` when the ``sqlalchemy_utc`` import is present.
Notable regressions locked in here:
* No migration exemption for ``sa.DateTime(timezone=True)``
(see ``test_no_migration_exemption_for_timezone_true``). This is the
inversion of the superseded PR #3515 approach.
* ``ast.IfExp`` branches are BOTH inspected — a violation in either
``body`` or ``orelse`` is caught.
* Path filter anchors on ``src/local_deep_research/`` so
``tests/database/models/`` is NOT scanned.
"""
import subprocess
import sys
import tempfile
from pathlib import Path
HOOK_SCRIPT = (
Path(__file__).parent.parent.parent
/ "scripts"
/ "pre_commit"
/ "check_datetime_timezone.py"
)
PACKAGE_PREFIX = ("src", "local_deep_research")
def _run_hook_on_source(
source: str,
subpath: tuple = PACKAGE_PREFIX + ("database", "models"),
filename: str = "example.py",
) -> subprocess.CompletedProcess:
"""Write *source* into a directory tree matching *subpath* and run the hook."""
with tempfile.TemporaryDirectory() as base:
nested = Path(base).joinpath(*subpath)
nested.mkdir(parents=True)
target = nested / filename
target.write_text(source)
return subprocess.run(
[sys.executable, str(HOOK_SCRIPT), str(target)],
capture_output=True,
text=True,
timeout=30,
)
def _run_hook_on_files(
files: list,
) -> subprocess.CompletedProcess:
"""Run the hook against multiple prepared files.
``files`` is a list of ``(subpath_tuple, filename, source)`` triples.
Returns after running the hook once with all file paths as arguments.
"""
with tempfile.TemporaryDirectory() as base:
paths = []
for subpath, filename, source in files:
nested = Path(base).joinpath(*subpath)
nested.mkdir(parents=True, exist_ok=True)
target = nested / filename
target.write_text(source)
paths.append(str(target))
return subprocess.run(
[sys.executable, str(HOOK_SCRIPT), *paths],
capture_output=True,
text=True,
timeout=30,
)
MODEL_PATH = PACKAGE_PREFIX + ("database", "models")
MIGRATION_PATH = PACKAGE_PREFIX + ("database", "migrations", "versions")
OUT_OF_SCOPE_PATH = ("tests", "database", "models")
IMPORT_LINE = "from sqlalchemy_utc import UtcDateTime\n"
IMPORT_LINE_COMBO = "from sqlalchemy_utc import utcnow, UtcDateTime\n"
# ---------------------------------------------------------------------------
# Flagged patterns
# ---------------------------------------------------------------------------
class TestFlagsViolations:
"""Patterns that must exit with returncode 1."""
def test_model_bare_datetime_call_flagged(self):
result = _run_hook_on_source(
"ts = Column(DateTime(), nullable=False)\n",
subpath=MODEL_PATH,
)
assert result.returncode == 1
assert "UtcDateTime" in result.stdout
def test_model_bare_datetime_name_flagged(self):
"""Bare ``Column(DateTime)`` without parens — new stricter handling."""
result = _run_hook_on_source(
"ts = Column(DateTime)\n",
subpath=MODEL_PATH,
)
assert result.returncode == 1
def test_model_utcdatetime_missing_import_flagged(self):
result = _run_hook_on_source(
"ts = Column(UtcDateTime, nullable=False)\n",
subpath=MODEL_PATH,
)
assert result.returncode == 1
assert "Missing import" in result.stdout
def test_no_migration_exemption_for_timezone_true(self):
"""PR #3515's migration exemption is intentionally removed.
A migration that uses ``sa.DateTime(timezone=True)`` is a
violation under the ``UtcDateTime``-everywhere rule, even though
the old hook behaviour accepted it.
"""
result = _run_hook_on_source(
'created_at = sa.Column("created_at", sa.DateTime(timezone=True), nullable=False)\n',
subpath=MIGRATION_PATH,
filename="0006_example.py",
)
assert result.returncode == 1
assert "UtcDateTime" in result.stdout
def test_migration_bare_datetime_flagged(self):
result = _run_hook_on_source(
'ts = sa.Column("ts", sa.DateTime())\n',
subpath=MIGRATION_PATH,
filename="0006_example.py",
)
assert result.returncode == 1
def test_conditional_type_violation_in_body(self):
"""``Column(DateTime() if cond else UtcDateTime())`` — body branch."""
source = IMPORT_LINE + (
"ts = Column(DateTime() if flag else UtcDateTime())\n"
)
result = _run_hook_on_source(source, subpath=MODEL_PATH)
assert result.returncode == 1
def test_conditional_type_violation_in_orelse(self):
"""``Column(UtcDateTime() if cond else DateTime())`` — orelse branch.
Regression guard for the asymmetric-branch bug: if the helper
short-circuits on the first resolved branch, this case slips
through silently.
"""
source = IMPORT_LINE + (
"ts = Column(UtcDateTime() if flag else DateTime())\n"
)
result = _run_hook_on_source(source, subpath=MODEL_PATH)
assert result.returncode == 1
def test_two_violations_in_one_file_both_reported(self):
source = (
"a = Column(DateTime(), nullable=False)\n"
"b = Column(DateTime(), nullable=False)\n"
)
result = _run_hook_on_source(source, subpath=MODEL_PATH)
assert result.returncode == 1
assert "Line 1" in result.stdout
assert "Line 2" in result.stdout
def test_batch_only_dirty_file_reported(self):
dirty = "ts = Column(DateTime(), nullable=False)\n"
clean = IMPORT_LINE + "ts = Column(UtcDateTime, nullable=False)\n"
result = _run_hook_on_files(
[
(MODEL_PATH, "dirty.py", dirty),
(MODEL_PATH, "clean.py", clean),
]
)
assert result.returncode == 1
assert "dirty.py" in result.stdout
assert "clean.py" not in result.stdout
def test_path_filter_scans_src_dir(self):
"""Positive side of the path-filter boundary."""
result = _run_hook_on_source(
"ts = Column(DateTime(), nullable=False)\n",
subpath=MODEL_PATH,
)
assert result.returncode == 1
# ---------------------------------------------------------------------------
# Allowed patterns
# ---------------------------------------------------------------------------
class TestAllowsCorrectPatterns:
"""Patterns that must exit with returncode 0."""
def test_model_utcdatetime_bare_with_import(self):
source = IMPORT_LINE + (
"ts = Column(UtcDateTime, default=utcnow(), nullable=False)\n"
)
result = _run_hook_on_source(source, subpath=MODEL_PATH)
assert result.returncode == 0
def test_migration_utcdatetime_call_with_import(self):
source = IMPORT_LINE + (
'ts = sa.Column("ts", UtcDateTime(), nullable=False)\n'
)
result = _run_hook_on_source(
source, subpath=MIGRATION_PATH, filename="0006_example.py"
)
assert result.returncode == 0
def test_combo_import_order_detected(self):
"""``from sqlalchemy_utc import utcnow, UtcDateTime`` is accepted."""
source = IMPORT_LINE_COMBO + (
"ts = Column(UtcDateTime, default=utcnow(), nullable=False)\n"
)
result = _run_hook_on_source(source, subpath=MODEL_PATH)
assert result.returncode == 0
def test_empty_file(self):
result = _run_hook_on_source("", subpath=MODEL_PATH)
assert result.returncode == 0
def test_syntax_error_file_does_not_crash(self):
"""Hook's ast.parse swallows SyntaxError and returns 0 for that file."""
result = _run_hook_on_source("def foo(:\n", subpath=MODEL_PATH)
assert result.returncode == 0
# ---------------------------------------------------------------------------
# Path filter
# ---------------------------------------------------------------------------
class TestPathFilter:
"""The hook must only scan files under the package prefix."""
def test_file_outside_both_dirs_is_skipped(self):
"""Arbitrary path — hook does not scan."""
with tempfile.TemporaryDirectory() as base:
target = Path(base) / "random" / "file.py"
target.parent.mkdir(parents=True)
target.write_text("ts = Column(DateTime(), nullable=False)\n")
result = subprocess.run(
[sys.executable, str(HOOK_SCRIPT), str(target)],
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0
def test_tests_dir_with_database_models_segment_is_skipped(self):
"""``tests/database/models/foo.py`` must NOT be scanned.
Prevents the pre-existing substring fallback from false-firing
on test fixtures that happen to declare schema-like code.
"""
result = _run_hook_on_source(
"ts = Column(DateTime(), nullable=False)\n",
subpath=OUT_OF_SCOPE_PATH,
)
assert result.returncode == 0
def test_models_backup_substring_boundary(self):
"""``database/models_backup/`` must NOT match."""
result = _run_hook_on_source(
"ts = Column(DateTime(), nullable=False)\n",
subpath=PACKAGE_PREFIX + ("database", "models_backup"),
)
assert result.returncode == 0
# ---------------------------------------------------------------------------
# Output format
# ---------------------------------------------------------------------------
class TestOutputFormat:
"""Lock in the user-facing output so refactors don't silently change it."""
def test_violation_stdout_includes_filename(self):
result = _run_hook_on_source(
"ts = Column(DateTime(), nullable=False)\n",
subpath=MODEL_PATH,
filename="my_model.py",
)
assert result.returncode == 1
assert "my_model.py" in result.stdout
def test_violation_stdout_includes_fix_hint(self):
result = _run_hook_on_source(
"ts = Column(DateTime(), nullable=False)\n",
subpath=MODEL_PATH,
)
assert "UtcDateTime" in result.stdout
assert "sqlalchemy_utc" in result.stdout