From bc14cd2177e8f97aa633ff4f162424add71b3a8f Mon Sep 17 00:00:00 2001 From: changukyu Date: Sat, 8 Aug 2026 09:47:57 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20=EB=8B=A8=EC=9D=BC=20verify=20?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=ED=8A=B8=20=EC=8B=A0=EC=84=A4=20(pytest.ini?= =?UTF-8?q?=20+=20ruff.toml=20+=20verify.ps1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 린트와 테스트가 각각 수동 실행이었고, 어느 조합이 커밋 게이트인지 코드에 기록된 곳이 없었다. ruff 는 dev 의존성에도 없어 전역 환경에서만 돌고 있었다. - verify.ps1: ruff -> pytest 순서로 도는 단일 명령. -SkipLint / -Coverage - pytest.ini: testpaths, pythonpath(. src) 로 맨 pytest 도 동작, 300s 행 가드 - ruff.toml: py313 / line-length 120 / 기본 룰셋 명시 (ruff 업그레이드가 게이트를 조용히 넓히거나 좁히지 못하게) - requirements-dev.txt 에 ruff==0.12.0 핀 린트 18건 정리 (전부 무해하지만 게이트가 통과해야 하므로): - env_clients.py / store_enrichment.py: logger 정의를 임포트 아래로 (E402 15건) - test_sqlite_store.py: lambda 할당 -> def (E731 2건) - test_store_workflow.py: 미사용 임포트 제거 (F401) --- pytest.ini | 15 +++++ requirements-dev.txt | 3 + ruff.toml | 10 ++++ src/rights_filter/integrations/env_clients.py | 4 +- src/rights_filter/server/store_enrichment.py | 4 +- .../rights_filter/server/test_sqlite_store.py | 8 ++- .../server/test_store_workflow.py | 2 +- verify.ps1 | 57 +++++++++++++++++++ 8 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 pytest.ini create mode 100644 ruff.toml create mode 100644 verify.ps1 diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..bf883c3 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,15 @@ +[pytest] +testpaths = tests + +# `tests.rights_filter.server.test_sqlite_store` is imported by sibling specs for +# shared fakes, and every module under test lives in src/. Declaring both here +# means bare `pytest` works; without it only `python -m pytest` did (that form +# puts the CWD on sys.path as a side effect, which is easy to get wrong in CI). +pythonpath = . src + +# Hang guard, not a performance budget. The slowest single spec (browser upload +# + real analysis) runs well under a minute; this only fires when something +# deadlocks, so an unattended `verify` run fails instead of hanging forever. +timeout = 300 + +addopts = --strict-markers diff --git a/requirements-dev.txt b/requirements-dev.txt index 0ce3b7e..4dc30b3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,6 +3,9 @@ pytest==8.4.2 pytest-cov==6.0.0 pytest-timeout==2.4.0 +# Lint gate in verify.ps1. Pinned because ruff's default rule set moves between +# releases; ruff.toml states the selection, this states the version. +ruff==0.12.0 # Operator-GUI browser (E2E) tests. The Chromium binary is NOT pip-installed: # on the build/staging host run `playwright install chromium`, then bundle the # browser cache (PLAYWRIGHT_BROWSERS_PATH) into the offline artifact for the diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..b02d51f --- /dev/null +++ b/ruff.toml @@ -0,0 +1,10 @@ +# Pinned so `verify` produces the same result on every machine. Without a config +# file ruff falls back to its built-in defaults, which shift between releases — +# a lint gate that changes under you is not a gate. +target-version = "py313" +line-length = 120 + +[lint] +# Ruff's default set (pycodestyle errors + pyflakes). Stated explicitly rather +# than inherited so a ruff upgrade cannot silently widen or narrow the gate. +select = ["E4", "E7", "E9", "F"] diff --git a/src/rights_filter/integrations/env_clients.py b/src/rights_filter/integrations/env_clients.py index 7cc4b2f..0ca70b5 100644 --- a/src/rights_filter/integrations/env_clients.py +++ b/src/rights_filter/integrations/env_clients.py @@ -5,8 +5,6 @@ from dataclasses import dataclass from datetime import datetime from typing import Any, Mapping -logger = logging.getLogger(__name__) - from rights_filter.analysis.llm_assistance import ( DEFAULT_MODEL, InternalLlmAssistant, @@ -24,6 +22,8 @@ from rights_filter.integrations.google_custom_search import ( from rights_filter.integrations.naver_search import NaverOpenApiSearchClient, NaverSearchAdapter from rights_filter.integrations.search_policy import SearchApiPolicy +logger = logging.getLogger(__name__) + # Chosen by measurement against real stored evidence (54 rows, 12 selected) on # the GPU target, not by parameter count: # qwen2.5:0.5b-instruct 55.7s — echoed the shape example as its claim text diff --git a/src/rights_filter/server/store_enrichment.py b/src/rights_filter/server/store_enrichment.py index 3ed6f7f..381760a 100644 --- a/src/rights_filter/server/store_enrichment.py +++ b/src/rights_filter/server/store_enrichment.py @@ -15,8 +15,6 @@ from dataclasses import replace from datetime import datetime from typing import Any -logger = logging.getLogger(__name__) - from rights_filter.analysis.face_person_detection import ( HeuristicFacePersonDetector, face_person_evidence_data, @@ -48,6 +46,8 @@ from rights_filter.server.store_serialization import ( _query_history_status, ) +logger = logging.getLogger(__name__) + class StoreEnrichmentMixin: def _sync_provider_payloads(self) -> None: diff --git a/tests/rights_filter/server/test_sqlite_store.py b/tests/rights_filter/server/test_sqlite_store.py index 7f7deb5..b14c7e3 100644 --- a/tests/rights_filter/server/test_sqlite_store.py +++ b/tests/rights_filter/server/test_sqlite_store.py @@ -3171,7 +3171,9 @@ def test_search_result_candidate_image_similarity_respects_runtime_threshold( ): image_store = _png_image_store(tmp_path / "submissions") original_image = image_store.image_payload("SUB-PNG1").content - fetcher = lambda _: original_image + + def fetcher(_): + return original_image runtime = build_provider_runtime( env={"COPYRIGHTER_SEARCH_RESULT_SIMILARITY_THRESHOLD": "0.95"}, @@ -3287,7 +3289,9 @@ def test_sqlite_store_compares_unwrapped_search_result_image_urls_against_submis def test_search_result_similarity_collects_multiple_matches_within_limit(tmp_path: Path): image_store = _png_image_store(tmp_path / "submissions") original_image = image_store.image_payload("SUB-PNG1").content - fetcher = lambda _: original_image + + def fetcher(_): + return original_image runtime = build_provider_runtime( env={"COPYRIGHTER_SEARCH_RESULT_COMPARE_LIMIT": "3"}, diff --git a/tests/rights_filter/server/test_store_workflow.py b/tests/rights_filter/server/test_store_workflow.py index d43fb76..aa7210f 100644 --- a/tests/rights_filter/server/test_store_workflow.py +++ b/tests/rights_filter/server/test_store_workflow.py @@ -7,7 +7,7 @@ from rights_filter.server.sqlite_store import CopyrighterStore from rights_filter.server.store_backup import restore_backup from rights_filter.server.store_workflow import MAX_BATCH_DECISIONS -from tests.rights_filter.server.test_sqlite_store import _image_store, FakeJsonTransport +from tests.rights_filter.server.test_sqlite_store import FakeJsonTransport def _photo_store(root: Path, *, duplicate: bool = False): diff --git a/verify.ps1 b/verify.ps1 new file mode 100644 index 0000000..ca4c9c0 --- /dev/null +++ b/verify.ps1 @@ -0,0 +1,57 @@ +<# +.SYNOPSIS + The single verification gate for POSA Copyrighter. Run this before commit, + before a delivery build, and from CI — same command, same result everywhere. + +.DESCRIPTION + Runs, in order: + 1. ruff — lint (config: ruff.toml) + 2. pytest — full suite incl. operator-GUI E2E (config: pytest.ini) + + Browser (Playwright) specs skip cleanly when Chromium is absent, so this + works on a host that has not run `playwright install chromium`. + +.PARAMETER SkipLint + Run tests only. For a fast inner loop; the commit gate runs both. + +.PARAMETER Coverage + Add a coverage report over src/. + +.EXAMPLE + .\verify.ps1 +.EXAMPLE + .\verify.ps1 -Coverage +#> +[CmdletBinding()] +param( + [switch]$SkipLint, + [switch]$Coverage +) + +$ErrorActionPreference = 'Stop' +Set-Location -LiteralPath $PSScriptRoot + +$failures = @() + +if (-not $SkipLint) { + Write-Host "`n=== ruff check ===" -ForegroundColor Cyan + & python -m ruff check src tests + if ($LASTEXITCODE -ne 0) { $failures += 'ruff' } +} + +Write-Host "`n=== pytest ===" -ForegroundColor Cyan +$pytestArgs = @() +if ($Coverage) { + $pytestArgs += @('--cov=src/rights_filter', '--cov-report=term-missing') +} +& python -m pytest @pytestArgs +if ($LASTEXITCODE -ne 0) { $failures += 'pytest' } + +Write-Host '' +if ($failures.Count -gt 0) { + Write-Host "VERIFY FAILED: $($failures -join ', ')" -ForegroundColor Red + exit 1 +} + +Write-Host 'VERIFY OK' -ForegroundColor Green +exit 0