Skip to content
Open
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
8 changes: 8 additions & 0 deletions docs/source/Support/Developer/CodingGuidlines.rst
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,14 @@ The default packages include ``pytest`` which is a program that can run a series
python scripts that begin with ``test_``. The package ``pytest-xdist`` is also installed by default
and allows these tests to be run in a multi-threaded manner using the argument ``-n auto``.

The ``src`` pytest configuration sets ``MPLCONFIGDIR`` to the checkout's
``.pytest_cache/matplotlib`` directory before importing Matplotlib. This ignored,
reusable cache avoids rebuilding the font cache on repeated runs when the home
directory is not writable, and is shared with local parallel test workers and CI
workers. An explicitly supplied ``MPLCONFIGDIR`` is preserved. If the checkout
cache cannot be written, Matplotlib uses its normal cache selection. The first
run after creating or clearing the cache still builds it.

Further, Basilisk uses the Google ``gtest`` suite to run unit tests on C or C++ support libraries.
To run all the unit and integrated test open a terminal window and change your working directory
to the root ``basilisk`` directory. Activate the python virtual environment if needed.
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
- Added :ref:`ommHandling` to read CCSDS Orbit Mean-Elements Message (OMM) general perturbations data in all four CelesTrak encodings (XML, JSON, CSV and KVN) and convert it to osculating orbital elements, lifting the five-digit catalog number limit of the TLE format. Includes :ref:`scenarioOrbitFromOmm`, a tutorial with bundled synthetic OMM data, spacecraft initialization, and orbit, altitude and speed figures.
- Added :ref:`ommHandling` to read CCSDS Orbit Mean-Elements Message (OMM) general perturbations data in all four CelesTrak encodings (XML, JSON, CSV and KVN) and convert it to osculating orbital elements, supporting catalog numbers through ``999999999`` with optional leading plus signs and preserved leading zeros. XML support includes unqualified elements and CCSDS namespace-qualified elements using prefixes or a default namespace, with per-record rejection of ambiguous duplicate fields and blocks (GitHub issue #1669). Supports standalone OMM and NDM containers, ignores segments inside extensions or other message types, and validates consumed XML field placement throughout recognized containers, including headers and optional data blocks. Rejects ambiguous duplicate consumed fields across all four encodings. CSV support includes leading blank lines and quoted multiline fields, with per-record validation of row widths. Accepts CelesTrak's ``SGP/SGP4`` mean-element theory label as ``SGP4`` across all four encodings (GitHub issue #1673). Accepts records with missing, null or blank catalog numbers without changing their orbital conversion, preserving each unidentified record separately. Preserves unavailable catalog numbers and international designators as empty metadata and retains supplied ``UNKNOWN`` designators. Documents the unscaled mean-motion derivative fields and CelesTrak's TLE coefficient convention. Includes :ref:`scenarioOrbitFromOmm`, a tutorial with bundled synthetic OMM data, spacecraft initialization, and orbit, altitude and speed figures.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Python tests now reuse a writable checkout-local Matplotlib cache to avoid repeated font-cache construction when the home directory is not writable, while preserving an explicit ``MPLCONFIGDIR`` setting.
24 changes: 24 additions & 0 deletions src/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,31 @@
import shutil
import subprocess
import sys
import tempfile
from datetime import date
from pathlib import Path


def _configure_matplotlib_cache(cache_directory):
"""Select a reusable, writable test cache unless the caller configured one.

:param cache_directory: checkout-local directory for Matplotlib configuration and caches
"""
if "MPLCONFIGDIR" in os.environ:
return
try:
cache_directory.mkdir(parents=True, exist_ok=True)
# Check actual writes, including on read-only mounts and Windows.
with tempfile.TemporaryFile(dir=cache_directory):
pass
except OSError:
# A read-only checkout must not prevent Matplotlib's normal fallback.
return
os.environ["MPLCONFIGDIR"] = str(cache_directory)


# Configure the cache before importing Matplotlib or pyplot. Workers inherit it.
_configure_matplotlib_cache(Path(__file__).resolve().parents[1] / ".pytest_cache" / "matplotlib")

import matplotlib as mpl
import matplotlib.pyplot as plt
Expand Down
127 changes: 127 additions & 0 deletions src/tests/test_pytest_matplotlib_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# ISC License
#
# Copyright (c) 2026, Autonomous Vehicle Systems Lab, University of Colorado at Boulder
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

"""Check persistent Matplotlib cache selection before plotting imports."""

import json
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

import conftest
import pytest


def test_matplotlib_cache_is_reused(tmp_path, monkeypatch):
"""Repeated test sessions select the same directory without deleting its contents."""
cache = tmp_path / "cache" / "matplotlib"
monkeypatch.delenv("MPLCONFIGDIR", raising=False)
conftest._configure_matplotlib_cache(cache)
assert os.environ["MPLCONFIGDIR"] == str(cache)
cached_file = cache / "cached-data"
cached_file.write_text("preserved", encoding="utf-8")
monkeypatch.delenv("MPLCONFIGDIR")
conftest._configure_matplotlib_cache(cache)
assert os.environ["MPLCONFIGDIR"] == str(cache)
assert cached_file.read_text(encoding="utf-8") == "preserved"
assert list(cache.iterdir()) == [cached_file]


@pytest.mark.parametrize("configured", ["custom-cache", ""])
def test_matplotlib_cache_preserves_environment(tmp_path, monkeypatch, configured):
"""An explicit environment setting takes precedence without creating another cache."""
cache = tmp_path / "unused"
monkeypatch.setenv("MPLCONFIGDIR", configured)
conftest._configure_matplotlib_cache(cache)
assert os.environ["MPLCONFIGDIR"] == configured
assert not cache.exists()


@pytest.mark.parametrize("failure", ["directory", "write"])
def test_matplotlib_cache_unwritable_checkout(tmp_path, monkeypatch, failure):
"""Directory creation or write failures leave Matplotlib free to choose its cache."""
cache = tmp_path / "matplotlib"
monkeypatch.delenv("MPLCONFIGDIR", raising=False)
if failure == "directory":
cache.write_text("a file blocks directory creation", encoding="utf-8")
else:
def denied_write(*args, **kwargs):
raise PermissionError("read-only cache")

monkeypatch.setattr(conftest.tempfile, "TemporaryFile", denied_write)
conftest._configure_matplotlib_cache(cache)
assert "MPLCONFIGDIR" not in os.environ


@pytest.mark.parametrize("symlink_cache", [False, True], ids=["directory", "symlink"])
def test_matplotlib_font_cache_survives_new_processes(tmp_path, symlink_cache):
"""Fresh processes reuse a direct or symlinked font cache independently of their cwd."""
configuration = Path(conftest.__file__).resolve()
cache = configuration.parents[1] / ".pytest_cache" / "matplotlib"
try:
cache.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryFile(dir=cache):
pass
except OSError:
pytest.skip("Font-cache integration requires a writable checkout")

if symlink_cache:
# Copy the configuration so __file__ identifies an isolated checkout, while
# its cache link shares the persistent cache instead of building a new one.
checkout = tmp_path / "checkout"
source = checkout / "src"
source.mkdir(parents=True)
for name in ("conftest.py", "reportconf.py"):
shutil.copy2(configuration.with_name(name), source / name)
linked_cache = checkout / ".pytest_cache"
try:
linked_cache.symlink_to(cache.parent, target_is_directory=True)
except OSError as error:
pytest.skip(f"Symbolic links are unavailable: {error}")
configuration = source / "conftest.py"
cache = linked_cache / "matplotlib"

script = """
import json
import runpy
import sys
from pathlib import Path

runpy.run_path(sys.argv[1])
import matplotlib
from matplotlib import font_manager

cache = Path(matplotlib.get_cachedir())
print(json.dumps({"directory": str(cache), "files": {
path.name: path.stat().st_mtime_ns for path in cache.glob("fontlist-v*.json")
}}))
"""
environment = os.environ.copy()
environment.pop("MPLCONFIGDIR", None)
environment["MPLBACKEND"] = "Agg"
results = []
for _ in range(2):
completed = subprocess.run(
[sys.executable, "-c", script, str(configuration)], cwd=tmp_path,
env=environment, check=True, capture_output=True, text=True,
)
results.append(json.loads(completed.stdout.splitlines()[-1]))
assert Path(results[0]["directory"]).samefile(cache)
assert results[0]["files"]
assert results[1] == results[0]
Loading
Loading