Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 21 additions & 0 deletions xrspatial/geotiff/tests/golden_corpus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
62 changes: 62 additions & 0 deletions xrspatial/geotiff/tests/golden_corpus/_marks.py
Original file line number Diff line number Diff line change
@@ -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 []
25 changes: 12 additions & 13 deletions xrspatial/geotiff/tests/test_golden_corpus_eager_numpy_1930.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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()
Expand Down
Loading