From 1c4c4f8d7d78a438c4a9a4d02ddf52766b00ad85 Mon Sep 17 00:00:00 2001 From: Nathaniel Starkman Date: Wed, 5 Feb 2025 23:06:23 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20test:=20set=20up=20figure=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nathaniel Starkman --- .github/workflows/ci.yml | 38 ++++++-- .gitignore | 3 + conftest.py | 32 +++++-- noxfile.py | 66 +++++++++++--- pyproject.toml | 15 ++- .../test_kepler_potential_contours.png | Bin 10194 -> 0 bytes .../matplotlib/test_dynamicssolver.py | 86 ++++++++++++++++++ tests/integration/matplotlib/test_orbit.py | 8 +- .../integration/matplotlib/test_potential.py | 4 +- tests/mpl_figure/hashes.json | 9 ++ uv.lock | 21 ++++- 11 files changed, 241 insertions(+), 41 deletions(-) delete mode 100644 tests/integration/matplotlib/baseline/test_kepler_potential_contours.png create mode 100644 tests/integration/matplotlib/test_dynamicssolver.py create mode 100644 tests/mpl_figure/hashes.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3d0cb16..2b8ce335 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,16 +67,8 @@ jobs: python-version: ["3.11", "3.13"] runs-on: [ubuntu-latest, macos-latest, windows-latest] - # TODO: figure out OpenBLAS install - # ``ERROR: Dependency "OpenBLAS" not found, tried pkgconfig and cmake`` - # include: - # - python-version: pypy-3.11 - # runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v5 @@ -113,8 +105,6 @@ jobs: steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v5 @@ -139,3 +129,31 @@ jobs: uses: codecov/codecov-action@v5.3.1 with: token: ${{ secrets.CODECOV_TOKEN }} + + check_figures: + name: Check Figures + runs-on: ${{ matrix.runs-on }} + needs: [smoke] + strategy: + fail-fast: false + matrix: + python-version: ["3.12"] + runs-on: [ubuntu-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install the project + run: uv sync --extra all --group test-all + + - name: Test package + run: uv run nox -s test_mpl diff --git a/.gitignore b/.gitignore index 37093200..41961d3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Project files +/tests/mpl_figure/baseline + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/conftest.py b/conftest.py index d539a261..ec07c071 100644 --- a/conftest.py +++ b/conftest.py @@ -1,21 +1,37 @@ """Doctest configuration.""" +from collections.abc import Callable, Iterable, Sequence from doctest import ELLIPSIS, NORMALIZE_WHITESPACE -from sybil import Sybil -from sybil.parsers.rest import DocTestParser, PythonCodeBlockParser, SkipParser +from sybil import Document, Region, Sybil +from sybil.parsers import myst, rest +from sybil.sybil import SybilCollection from optional_dependencies import OptionalDependencyEnum, auto from optional_dependencies.utils import chain_checks, get_version, is_installed -pytest_collect_file = Sybil( +optionflags = ELLIPSIS | NORMALIZE_WHITESPACE + +parsers: Sequence[Callable[[Document], Iterable[Region]]] = [ + myst.DocTestDirectiveParser(optionflags=optionflags), + myst.PythonCodeBlockParser(doctest_optionflags=optionflags), + myst.SkipParser(), +] + +readme = Sybil(parsers=parsers, patterns=["*.md"]) +docs = Sybil( parsers=[ - DocTestParser(optionflags=NORMALIZE_WHITESPACE | ELLIPSIS), - PythonCodeBlockParser(), - SkipParser(), + rest.DocTestParser(optionflags=NORMALIZE_WHITESPACE | ELLIPSIS), + rest.PythonCodeBlockParser(), + rest.SkipParser(), ], - patterns=["*.rst", "*.py"], -).pytest() + patterns=["*.rst"], +) +python = Sybil( + parsers=[*parsers, rest.DocTestParser(optionflags=optionflags)], patterns=["*.py"] +) + +pytest_collect_file = SybilCollection((readme, docs, python)).pytest() class OptDeps(OptionalDependencyEnum): diff --git a/noxfile.py b/noxfile.py index d532569c..0d136282 100644 --- a/noxfile.py +++ b/noxfile.py @@ -13,6 +13,9 @@ nox.options.sessions = ["lint", "tests", "doctests"] nox.options.default_venv_backend = "uv|virtualenv" +# =================================================================== +# Linting + @nox.session def lint(session: nox.Session) -> None: @@ -36,38 +39,73 @@ def pylint(session: nox.Session) -> None: session.run("pylint", "galax", *session.posargs) +# =================================================================== +# Testing + + @nox.session -def tests(session: nox.Session) -> None: - """Run the unit and regular tests.""" +def tests_standard(session: nox.Session) -> None: + """Run the regular tests: src, README, docs, tests/unit.""" session.install("-e", ".[test]") - os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1" # TODO: set in a better way - session.run("pytest", *session.posargs) + os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1" + session.run("pytest", "src", "README", "docs", "tests/unit", *session.posargs) @nox.session def tests_all(session: nox.Session) -> None: - """Run the unit and regular tests.""" + """Run all the tests.""" session.install("-e", ".[test-all]") - os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1" # TODO: set in a better way + os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1" session.run("pytest", *session.posargs) @nox.session def doctests(session: nox.Session) -> None: - """Run the regular tests and doctests.""" - session.install(".[test]") + """Run the doctests: README, docs, src -- including mpl tests.""" + session.install(".[test,test-mpl]") + os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1" + session.run( + "pytest", + *("README", "docs", "src/galax"), + "--mpl", + *session.posargs, + ) + + +@nox.session +def generate_mpl_tests(session: nox.Session) -> None: + """Generate the mpl tests.""" + session.install(".[test,test-mpl]") + os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1" + session.run( + "pytest", + "tests", + "-m mpl_image_compare", # only run the mpl tests + "--mpl-generate-hash-library=tests/mpl_figure/hashes.json", + "--mpl-generate-path=tests/mpl_figure/baseline", + "--mpl-generate-summary=html,json,basic-json", + *session.posargs, + ) + + +@nox.session +def test_mpl(session: nox.Session) -> None: + """Test the figures.""" + session.install(".[test,test-mpl]") + os.environ["GALAX_ENABLE_RUNTIME_TYPECHECKS"] = "1" session.run( "pytest", - "--doctest-modules", - '--doctest-glob="*.rst"', - '--doctest-glob="*.md"', - '--doctest-glob="*.py"', - "docs", - "src/galax", + "tests", + "--mpl", + "-m mpl_image_compare", # only run the mpl tests + "--mpl-generate-summary=basic-html,json", *session.posargs, ) +# =================================================================== + + @nox.session(reuse_venv=True) def docs(session: nox.Session) -> None: """Build the docs. Pass "--serve" to serve. Pass "-b linkcheck" to check links.""" diff --git a/pyproject.toml b/pyproject.toml index 720e49a6..bcb7d89c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,13 +103,17 @@ ] test = [ "nox>=2024.10.9", + "optype[numpy]>=0.9.0", "pre-commit>=4.0.1", "pytest-arraydiff>=0.6.1", "pytest-cov>=5", "pytest>=8.3", "sybil>=8.0.0", - ] - test-mpl = ["pytest-mpl>=0.17.0"] +] + test-mpl = [ + "pytest-mpl>=0.17.0", + "matplotlib==3.9.2", +] [tool.hatch] @@ -229,6 +233,13 @@ testpaths = ["docs", "src/galax", "tests/"] xfail_strict = true + # pytest-mpl settings + mpl-use-full-test-name = true + mpl-hash-library = "tests/mpl_figure/hashes.json" + mpl-baseline-path = "https://raw.githubusercontent.com/GalacticDynamics/galax-figure-tests/main/figures/" + mpl-deterministic = true + + [tool.ruff] diff --git a/tests/integration/matplotlib/baseline/test_kepler_potential_contours.png b/tests/integration/matplotlib/baseline/test_kepler_potential_contours.png deleted file mode 100644 index e5f917619952079f5054974d28b29ed70e6abca2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10194 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 AqyPW_ diff --git a/tests/integration/matplotlib/test_dynamicssolver.py b/tests/integration/matplotlib/test_dynamicssolver.py new file mode 100644 index 00000000..75718665 --- /dev/null +++ b/tests/integration/matplotlib/test_dynamicssolver.py @@ -0,0 +1,86 @@ +"""Test the `galax.dynamics.orbit` package contents.""" + +from itertools import combinations +from typing import Literal, TypeAlias + +import matplotlib.pyplot as plt +import numpy as np +import optype.numpy as onp +import pytest +from matplotlib.axes import Axes +from matplotlib.figure import Figure + +import quaxed.numpy as jnp +import unxt as u + +import galax.coordinates as gc +import galax.dynamics as gd +import galax.potential as gp + + +@pytest.fixture +def potential() -> gp.KeplerPotential: + """Kepler potential fixture.""" + return gp.KeplerPotential(m_tot=u.Quantity(1e12, "Msun"), units="galactic") + + +@pytest.fixture +def field(potential: gp.KeplerPotential) -> gd.fields.HamiltonianField: + """Hamiltonian field fixture.""" + return gd.fields.HamiltonianField(potential) + + +@pytest.fixture +def solver() -> gd.DynamicsSolver: + """Dynamics solver fixture.""" + return gd.DynamicsSolver() + + +FigAx23: TypeAlias = tuple[Figure, onp.Array[tuple[Literal[2], Literal[3]], Axes]] + + +@pytest.fixture +def sixaxfig() -> FigAx23: + """Six axis figure fixture.""" + fig, axs = plt.subplots(2, 3, figsize=(12, 8)) + return fig, axs + + +# ============================================================================= + + +@pytest.mark.mpl_image_compare +def test_solution_plot( + solver: gd.DynamicsSolver, field: gd.fields.HamiltonianField, sixaxfig: FigAx23 +) -> Figure: + """Test plotting an orbit in a Kepler potential.""" + # Solve the dynamical system + w0 = gc.PhaseSpacePosition( + q=u.Quantity([8.0, 0.0, 0.5], "kpc"), + p=u.Quantity([0.0, 220.0, 0.0], "km/s"), + t=u.Quantity(0.0, "Gyr"), + ) + tf = u.Quantity(200, "Myr") + saveat = jnp.linspace(w0.t, tf, 1000) + soln = solver.solve(field, w0, tf, saveat=saveat) + q, p = soln.ys + + # Plot the solution + fig, axs = sixaxfig + fig.suptitle("Phase space solution") + + usys = field.units + labels = np.empty((2, 3), dtype=" gd.Orbi # ============================================================================= -@pytest.mark.mpl_image_compare(deterministic=True) +@pytest.mark.mpl_image_compare def test_orbit_plot(orbit: gd.Orbit) -> Figure: """Test plotting an orbit in a Kepler potential.""" ax = orbit.plot(x="x", y="y") @@ -46,7 +46,7 @@ def test_orbit_plot(orbit: gd.Orbit) -> Figure: return ax.figure -@pytest.mark.mpl_image_compare(deterministic=True) +@pytest.mark.mpl_image_compare def test_orbit_plot_represent_as(orbit: gd.Orbit) -> Figure: """Test plotting an orbit in a Kepler potential.""" ax = orbit.plot(x="rho", y="d_z", vector_representation=cx.vecs.CylindricalPos) @@ -54,7 +54,7 @@ def test_orbit_plot_represent_as(orbit: gd.Orbit) -> Figure: return ax.figure -@pytest.mark.mpl_image_compare(deterministic=True) +@pytest.mark.mpl_image_compare def test_orbit_plot_scatter(orbit: gd.Orbit) -> Figure: """Test plotting an orbit in a Kepler potential.""" ax = orbit.plot(x="x", y="y", plot_function="scatter") @@ -62,7 +62,7 @@ def test_orbit_plot_scatter(orbit: gd.Orbit) -> Figure: return ax.figure -@pytest.mark.mpl_image_compare(deterministic=True) +@pytest.mark.mpl_image_compare def test_orbit_plot_time_color(orbit: gd.Orbit) -> Figure: """Test plotting an orbit in a Kepler potential.""" ax = orbit.plot(x="x", y="y", plot_function="scatter", c="orbit.t") diff --git a/tests/integration/matplotlib/test_potential.py b/tests/integration/matplotlib/test_potential.py index 52cd2fca..86e92236 100644 --- a/tests/integration/matplotlib/test_potential.py +++ b/tests/integration/matplotlib/test_potential.py @@ -9,7 +9,7 @@ import galax.potential as gp -@pytest.mark.mpl_image_compare(deterministic=True) +@pytest.mark.mpl_image_compare def test_kepler_potential_contours() -> Figure: """Test plotting Kepler potential contours.""" pot = gp.KeplerPotential( @@ -24,7 +24,7 @@ def test_kepler_potential_contours() -> Figure: return fig -@pytest.mark.mpl_image_compare(deterministic=True) +@pytest.mark.mpl_image_compare def test_kernel_density_contours() -> Figure: """Test plotting kernel density contours.""" pot = gp.KeplerPotential( diff --git a/tests/mpl_figure/hashes.json b/tests/mpl_figure/hashes.json new file mode 100644 index 00000000..b78d7218 --- /dev/null +++ b/tests/mpl_figure/hashes.json @@ -0,0 +1,9 @@ +{ + "integration.matplotlib.test_dynamicssolver.test_solution_plot": "cf2546a91582065e84f8e5494b31eff6219533bf7ce66000f835c5aaf9d6cc89", + "integration.matplotlib.test_orbit.test_orbit_plot": "4766db5ca76f7c347d4811587e3798f42eec1ae9234832459478d54e8563dbb0", + "integration.matplotlib.test_orbit.test_orbit_plot_represent_as": "83ca861ec069a9ddda18b09b469ed27c3b953dc87184116e17e2cb1f1c829c51", + "integration.matplotlib.test_orbit.test_orbit_plot_scatter": "e7f337382dada6558022a0888f2f741c90e74a6531ececa293e7754df273a4ee", + "integration.matplotlib.test_orbit.test_orbit_plot_time_color": "3a12252a7e2b676d8c3427b36b6d8c5753023e3a0b416b0d7433a9e7dcd91c92", + "integration.matplotlib.test_potential.test_kepler_potential_contours": "f21ac32338c8120f0f8851f1fef3c516bc906bb21452539868361cf86833e561", + "integration.matplotlib.test_potential.test_kernel_density_contours": "51f9e6ca266b063efdd9fd6914b09d689b9ff4be55554a5b0a981e54b972e8d3" +} diff --git a/uv.lock b/uv.lock index ecd88eee..3e20402f 100644 --- a/uv.lock +++ b/uv.lock @@ -729,8 +729,10 @@ dev = [ { name = "cz-conventional-gitmoji" }, { name = "furo" }, { name = "ipykernel" }, + { name = "matplotlib" }, { name = "myst-parser" }, { name = "nox" }, + { name = "optype", extra = ["numpy"] }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-arraydiff" }, @@ -750,6 +752,7 @@ docs = [ ] test = [ { name = "nox" }, + { name = "optype", extra = ["numpy"] }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-arraydiff" }, @@ -757,7 +760,9 @@ test = [ { name = "sybil" }, ] test-all = [ + { name = "matplotlib" }, { name = "nox" }, + { name = "optype", extra = ["numpy"] }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-arraydiff" }, @@ -766,6 +771,7 @@ test-all = [ { name = "sybil" }, ] test-mpl = [ + { name = "matplotlib" }, { name = "pytest-mpl" }, ] @@ -814,8 +820,10 @@ dev = [ { name = "cz-conventional-gitmoji", specifier = ">=0.6.1" }, { name = "furo", specifier = ">=2024.8.6" }, { name = "ipykernel", specifier = ">=6.29.5" }, + { name = "matplotlib", specifier = "==3.9.2" }, { name = "myst-parser", specifier = ">=4.0" }, { name = "nox", specifier = ">=2024.10.9" }, + { name = "optype", extras = ["numpy"], specifier = ">=0.9.0" }, { name = "pre-commit", specifier = ">=4.0.1" }, { name = "pytest", specifier = ">=8.3" }, { name = "pytest", specifier = ">=8.3.3" }, @@ -836,6 +844,7 @@ docs = [ ] test = [ { name = "nox", specifier = ">=2024.10.9" }, + { name = "optype", extras = ["numpy"], specifier = ">=0.9.0" }, { name = "pre-commit", specifier = ">=4.0.1" }, { name = "pytest", specifier = ">=8.3" }, { name = "pytest-arraydiff", specifier = ">=0.6.1" }, @@ -843,7 +852,9 @@ test = [ { name = "sybil", specifier = ">=8.0.0" }, ] test-all = [ + { name = "matplotlib", specifier = "==3.9.2" }, { name = "nox", specifier = ">=2024.10.9" }, + { name = "optype", extras = ["numpy"], specifier = ">=0.9.0" }, { name = "pre-commit", specifier = ">=4.0.1" }, { name = "pytest", specifier = ">=8.3" }, { name = "pytest-arraydiff", specifier = ">=0.6.1" }, @@ -851,7 +862,10 @@ test-all = [ { name = "pytest-mpl", specifier = ">=0.17.0" }, { name = "sybil", specifier = ">=8.0.0" }, ] -test-mpl = [{ name = "pytest-mpl", specifier = ">=0.17.0" }] +test-mpl = [ + { name = "matplotlib", specifier = "==3.9.2" }, + { name = "pytest-mpl", specifier = ">=0.17.0" }, +] [[package]] name = "galpy" @@ -1428,6 +1442,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/c2/0d5694ea30f6b852955a86e13e6a28387f60079b0b10e2ca9658fbf5cca5/optype-0.9.0-py3-none-any.whl", hash = "sha256:19dbbb71622961e903e60c12f370d910498e81bc4ae8b40d18c94dd106f4ce6b", size = 81554 }, ] +[package.optional-dependencies] +numpy = [ + { name = "numpy" }, +] + [[package]] name = "packaging" version = "24.1"