chore: 단일 verify 게이트 신설 (pytest.ini + ruff.toml + verify.ps1)
린트와 테스트가 각각 수동 실행이었고, 어느 조합이 커밋 게이트인지 코드에 기록된 곳이 없었다. 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)
This commit is contained in:
parent
fa00f382e2
commit
bc14cd2177
8 changed files with 96 additions and 7 deletions
15
pytest.ini
Normal file
15
pytest.ini
Normal file
|
|
@ -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
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
pytest==8.4.2
|
pytest==8.4.2
|
||||||
pytest-cov==6.0.0
|
pytest-cov==6.0.0
|
||||||
pytest-timeout==2.4.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:
|
# Operator-GUI browser (E2E) tests. The Chromium binary is NOT pip-installed:
|
||||||
# on the build/staging host run `playwright install chromium`, then bundle the
|
# on the build/staging host run `playwright install chromium`, then bundle the
|
||||||
# browser cache (PLAYWRIGHT_BROWSERS_PATH) into the offline artifact for the
|
# browser cache (PLAYWRIGHT_BROWSERS_PATH) into the offline artifact for the
|
||||||
|
|
|
||||||
10
ruff.toml
Normal file
10
ruff.toml
Normal file
|
|
@ -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"]
|
||||||
|
|
@ -5,8 +5,6 @@ from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Mapping
|
from typing import Any, Mapping
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
from rights_filter.analysis.llm_assistance import (
|
from rights_filter.analysis.llm_assistance import (
|
||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
InternalLlmAssistant,
|
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.naver_search import NaverOpenApiSearchClient, NaverSearchAdapter
|
||||||
from rights_filter.integrations.search_policy import SearchApiPolicy
|
from rights_filter.integrations.search_policy import SearchApiPolicy
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Chosen by measurement against real stored evidence (54 rows, 12 selected) on
|
# Chosen by measurement against real stored evidence (54 rows, 12 selected) on
|
||||||
# the GPU target, not by parameter count:
|
# the GPU target, not by parameter count:
|
||||||
# qwen2.5:0.5b-instruct 55.7s — echoed the shape example as its claim text
|
# qwen2.5:0.5b-instruct 55.7s — echoed the shape example as its claim text
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@ from dataclasses import replace
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
from rights_filter.analysis.face_person_detection import (
|
from rights_filter.analysis.face_person_detection import (
|
||||||
HeuristicFacePersonDetector,
|
HeuristicFacePersonDetector,
|
||||||
face_person_evidence_data,
|
face_person_evidence_data,
|
||||||
|
|
@ -48,6 +46,8 @@ from rights_filter.server.store_serialization import (
|
||||||
_query_history_status,
|
_query_history_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class StoreEnrichmentMixin:
|
class StoreEnrichmentMixin:
|
||||||
def _sync_provider_payloads(self) -> None:
|
def _sync_provider_payloads(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -3171,7 +3171,9 @@ def test_search_result_candidate_image_similarity_respects_runtime_threshold(
|
||||||
):
|
):
|
||||||
image_store = _png_image_store(tmp_path / "submissions")
|
image_store = _png_image_store(tmp_path / "submissions")
|
||||||
original_image = image_store.image_payload("SUB-PNG1").content
|
original_image = image_store.image_payload("SUB-PNG1").content
|
||||||
fetcher = lambda _: original_image
|
|
||||||
|
def fetcher(_):
|
||||||
|
return original_image
|
||||||
|
|
||||||
runtime = build_provider_runtime(
|
runtime = build_provider_runtime(
|
||||||
env={"COPYRIGHTER_SEARCH_RESULT_SIMILARITY_THRESHOLD": "0.95"},
|
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):
|
def test_search_result_similarity_collects_multiple_matches_within_limit(tmp_path: Path):
|
||||||
image_store = _png_image_store(tmp_path / "submissions")
|
image_store = _png_image_store(tmp_path / "submissions")
|
||||||
original_image = image_store.image_payload("SUB-PNG1").content
|
original_image = image_store.image_payload("SUB-PNG1").content
|
||||||
fetcher = lambda _: original_image
|
|
||||||
|
def fetcher(_):
|
||||||
|
return original_image
|
||||||
|
|
||||||
runtime = build_provider_runtime(
|
runtime = build_provider_runtime(
|
||||||
env={"COPYRIGHTER_SEARCH_RESULT_COMPARE_LIMIT": "3"},
|
env={"COPYRIGHTER_SEARCH_RESULT_COMPARE_LIMIT": "3"},
|
||||||
|
|
|
||||||
|
|
@ -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_backup import restore_backup
|
||||||
from rights_filter.server.store_workflow import MAX_BATCH_DECISIONS
|
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):
|
def _photo_store(root: Path, *, duplicate: bool = False):
|
||||||
|
|
|
||||||
57
verify.ps1
Normal file
57
verify.ps1
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in a new issue