diff --git a/setup.cfg b/setup.cfg index 7f570c745..224ebf11b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -103,6 +103,8 @@ filterwarnings = ignore:'resetCache' deprecated:DeprecationWarning:matplotlib ignore:'enablePackrat' deprecated:DeprecationWarning:matplotlib ignore:'asyncio.AbstractEventLoopPolicy' is deprecated:DeprecationWarning:pytest_asyncio +markers = + slow: long-running test cell (typical: golden-corpus fixtures behind a heavy codec or large pixel count). PR CI can skip with `-m "not slow"`; nightly / release runs use no filter. See xrspatial/geotiff/tests/golden_corpus/_marks.py for the corpus-side helper. [isort] line_length = 100 diff --git a/xrspatial/geotiff/tests/golden_corpus/README.md b/xrspatial/geotiff/tests/golden_corpus/README.md index 411bc31a8..f118d9285 100644 --- a/xrspatial/geotiff/tests/golden_corpus/README.md +++ b/xrspatial/geotiff/tests/golden_corpus/README.md @@ -46,6 +46,27 @@ iteration, and mtimes normalised to a fixed epoch. The schema is documented in the comments at the top of `manifest.yaml` and enforced by `generate.validate()`. +## Fast / slow split + +Each fixture's `tags:` list controls whether it runs in the PR CI fast +lane. A fixture is **fast** if `"fast"` appears in its `tags`. Everything +else picks up `pytest.mark.slow` automatically via the helper in +`_marks.py`, which the per-backend test modules consume from their +`_build_param`. + +* `pytest`: runs every cell, fast and slow. +* `pytest -m "not slow"`: PR fast lane; skips heavy cells. +* `pytest -m slow`: only the slow cells, e.g. for a nightly job that + exercises the long tail. + +Today most shipped fixtures carry `fast`. The six `compression_*` +fixtures in the manifest do not, so `pytest -m "not slow"` deselects +them. A one-line manifest edit per fixture would move them into the +fast lane if the team decides that is the right calibration. Future +heavier fixtures (large COGs, jpeg2000, multi-source VRTs) drop in +behind the same boundary without re-plumbing each backend test +module. + ## What is deliberately not in this PR * Real fixture files. Phase 2 PRs add them in batches (tiled/stripped, diff --git a/xrspatial/geotiff/tests/golden_corpus/_marks.py b/xrspatial/geotiff/tests/golden_corpus/_marks.py new file mode 100644 index 000000000..7ec3cc64f --- /dev/null +++ b/xrspatial/geotiff/tests/golden_corpus/_marks.py @@ -0,0 +1,62 @@ +"""Fast / slow pytest marker helper for the golden-corpus matrix +(issue #1930, phase 4 PR 1). + +Each manifest fixture carries a ``tags`` list. Fixtures tagged ``fast`` +run in the PR CI fast lane; everything else is treated as slow and +gets ``pytest.mark.slow`` attached so PR CI can opt out via +``pytest -m "not slow"``. Nightly / release CI runs without the filter +and exercises everything. + +Today most shipped fixtures carry ``fast``. The six ``compression_*`` +fixtures in the manifest do not, so they land in the slow lane and +``pytest -m "not slow"`` deselects them. A one-line manifest edit per +fixture would move them to the fast lane if the team decides that is +the right calibration. Future heavier fixtures (large COGs, +multi-source VRTs, jpeg2000 cells) will drop in behind the same +boundary without each backend test module re-implementing it. + +Usage from a backend test module:: + + from xrspatial.geotiff.tests.golden_corpus._marks import ( + fast_slow_marks_for, + ) + + def _build_param(entry): + marks = list(fast_slow_marks_for(entry)) + if entry["id"] in _PARITY_GAPS: + marks.append(pytest.mark.xfail(...)) + return pytest.param(entry, id=entry["id"], marks=marks) + +``fast_slow_marks_for`` is a generator, so chaining with other marks +is just a ``list(...) + [extra_mark]`` away. +""" +from __future__ import annotations + +from typing import Any + +import pytest + + +_FAST_TAG = "fast" + + +def is_fast(entry: dict[str, Any]) -> bool: + """Return True when the manifest entry is in the fast lane. + + The contract: a fixture is fast iff its ``tags`` list contains the + literal string ``"fast"``. Missing or empty ``tags`` count as slow, + on the theory that an untagged fixture is one a contributor forgot + to triage rather than one that is intentionally cheap. + """ + tags = entry.get("tags") or [] + return _FAST_TAG in tags + + +def fast_slow_marks_for(entry: dict[str, Any]) -> list[pytest.MarkDecorator]: + """Return the slow mark (in a list) when the entry is not fast. + + Returns ``[pytest.mark.slow]`` for slow fixtures and ``[]`` for fast + ones, so the caller can splat the result into its ``marks=`` list + without an empty-mark guard or a generator-to-list conversion. + """ + return [pytest.mark.slow] if not is_fast(entry) else [] diff --git a/xrspatial/geotiff/tests/test_golden_corpus_eager_numpy_1930.py b/xrspatial/geotiff/tests/test_golden_corpus_eager_numpy_1930.py index a48c3c61b..8b4cea535 100644 --- a/xrspatial/geotiff/tests/test_golden_corpus_eager_numpy_1930.py +++ b/xrspatial/geotiff/tests/test_golden_corpus_eager_numpy_1930.py @@ -61,6 +61,9 @@ from xrspatial.geotiff import open_geotiff # noqa: E402 from xrspatial.geotiff.tests.golden_corpus import generate # noqa: E402 +from xrspatial.geotiff.tests.golden_corpus._marks import ( # noqa: E402 + fast_slow_marks_for, +) from xrspatial.geotiff.tests.golden_corpus._oracle import ( # noqa: E402 compare_to_oracle, ) @@ -126,26 +129,22 @@ def _is_lossy(entry: dict) -> bool: def _build_param(entry: dict) -> pytest.param: - """Wrap a fixture entry in a ``pytest.param`` with the right mark. + """Wrap a fixture entry in a ``pytest.param`` with the right marks. Real parity gaps get ``xfail(strict=True)`` so the test surfaces a hard failure the day the gap closes. The MinIsWhite cell gets a plain skip - because the divergence is intentional. + because the divergence is intentional. Non-fast fixtures additionally + pick up ``pytest.mark.slow`` from the corpus helper. """ fid = entry["id"] + marks = list(fast_slow_marks_for(entry)) if fid in _PARITY_GAPS: - return pytest.param( - entry, - id=fid, - marks=pytest.mark.xfail(reason=_PARITY_GAPS[fid], strict=True), - ) - if fid in _INTENTIONAL_SKIPS: - return pytest.param( - entry, - id=fid, - marks=pytest.mark.skip(reason=_INTENTIONAL_SKIPS[fid]), + marks.append( + pytest.mark.xfail(reason=_PARITY_GAPS[fid], strict=True) ) - return pytest.param(entry, id=fid) + elif fid in _INTENTIONAL_SKIPS: + marks.append(pytest.mark.skip(reason=_INTENTIONAL_SKIPS[fid])) + return pytest.param(entry, id=fid, marks=marks) _FIXTURES = _resolved_fixtures()