diff --git a/docs/examples/ch03NiModelling/solutions/diffpy-cmi/fitBulkNi.py b/docs/examples/ch03NiModelling/solutions/diffpy-cmi/fitBulkNi.py index 5ff8f65..9351535 100644 --- a/docs/examples/ch03NiModelling/solutions/diffpy-cmi/fitBulkNi.py +++ b/docs/examples/ch03NiModelling/solutions/diffpy-cmi/fitBulkNi.py @@ -70,7 +70,7 @@ # If we want to run using multiprocessors, we can switch this to 'True'. # This requires that the 'psutil' python package installed. -RUN_PARALLEL = True +RUN_PARALLEL = False # Functions that will carry out the refinement ################## diff --git a/docs/examples/ch03NiModelling/solutions/diffpy-cmi/fitNPPt.py b/docs/examples/ch03NiModelling/solutions/diffpy-cmi/fitNPPt.py index 35f164d..7f9c6a8 100644 --- a/docs/examples/ch03NiModelling/solutions/diffpy-cmi/fitNPPt.py +++ b/docs/examples/ch03NiModelling/solutions/diffpy-cmi/fitNPPt.py @@ -87,6 +87,9 @@ print("The Ni example refines instrument parameters\n") print("The instrument parameters are necessary to run this fit\n") print("Please run the Ni example first\n") + print("Setting Q_damp and Q_broad to refined values\n") + QDAMP_I = 0.045298 + QBROAD_I = 0.016809 # If we want to run using multiprocessors, we can switch this to 'True'. # This requires that the 'psutil' python package installed. diff --git a/docs/examples/ch11ClusterXYZ/solutions/diffpy-cmi/fitCdSeNP.py b/docs/examples/ch11ClusterXYZ/solutions/diffpy-cmi/fitCdSeNP.py index 316948b..98fdae0 100644 --- a/docs/examples/ch11ClusterXYZ/solutions/diffpy-cmi/fitCdSeNP.py +++ b/docs/examples/ch11ClusterXYZ/solutions/diffpy-cmi/fitCdSeNP.py @@ -199,9 +199,6 @@ def plot_results(recipe, figname): diff = g - gcalc + diffzero mpl.rcParams.update(mpl.rcParamsDefault) - plt.style.use( - str(PWD.parent.parent.parent / "utils" / "billinge.mplstyle") - ) fig, ax1 = plt.subplots(1, 1) diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst index 487326c..28444ee 100644 --- a/docs/source/tutorials/index.rst +++ b/docs/source/tutorials/index.rst @@ -6,7 +6,7 @@ Despite its utility, PDF fitting and analysis can be challenging. The process of understand PDFs requires time, care, and the development of intuition to connect structural models to experimental data. Hopefully by the end of this tutorial series, PDF fitting will go from being a mysterious black box to a powerful tool in your structural analysis. -Example usage of ``diffpy.cmi`` can be found at `this GitHub repo `_. +Example usage of ``diffpy.cmi`` can be found at `this GitHub repo `_. .. _structural-parameters: diff --git a/news/test-tutorial-CI.rst b/news/test-tutorial-CI.rst new file mode 100644 index 0000000..9dfda0f --- /dev/null +++ b/news/test-tutorial-CI.rst @@ -0,0 +1,23 @@ +**Added:** + +* Add CI for testing examples of the PDF pack. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/pyproject.toml b/pyproject.toml index 55c71c2..eb745d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,13 @@ include = ["*"] # package names should match these glob patterns (["*"] by defa exclude = [] # exclude packages matching these glob patterns (empty by default) namespaces = false # to disable scanning PEP 420 namespaces (true by default) +[tool.coverage.run] +omit = [ + "*/tests/*", + "*/examples_copy*/*", + "*/__pycache__/*", +] + [project.scripts] cmi = "diffpy.cmi.cli:main" diff --git a/tests/conftest.py b/tests/conftest.py index e3b6313..a4f19e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,19 @@ import json +import shutil +import warnings from pathlib import Path +import matplotlib import pytest +# Suppress specific UserWarnings when running mpl headless +warnings.filterwarnings( + "ignore", category=UserWarning, message=".*FigureCanvasAgg.*" +) + +EXAMPLES_DIR = Path(__file__).parent.parent / "docs" / "examples" +TESTS_DIR = Path(__file__).parent + @pytest.fixture def user_filesystem(tmp_path): @@ -17,3 +28,22 @@ def user_filesystem(tmp_path): json.dump(home_config_data, f) yield tmp_path + + +@pytest.fixture(scope="session", autouse=True) +def use_headless_matplotlib(): + """Force matplotlib to use a headless backend during tests.""" + matplotlib.use("Agg") + + +@pytest.fixture(scope="session") +def examples_tmpdir(tmp_path_factory): + """Make a temp copy of all examples for safe testing. + + Removes the temp copy after tests are done. + """ + temp_dir = tmp_path_factory.mktemp("examples_copy") + temp_examples = temp_dir / "examples" + shutil.copytree(EXAMPLES_DIR, temp_examples, dirs_exist_ok=True) + yield temp_examples + shutil.rmtree(temp_dir, ignore_errors=True) diff --git a/tests/examples_exist_test.py b/tests/examples_exist_test.py new file mode 100644 index 0000000..62ce979 --- /dev/null +++ b/tests/examples_exist_test.py @@ -0,0 +1,25 @@ +from conftest import TESTS_DIR + + +def test_example_dirs_have_tests(examples_tmpdir): + """Verify that each example directory has a corresponding test + file.""" + example_dirs = [d for d in examples_tmpdir.iterdir() if d.is_dir()] + missing_tests = [] + for example_dir in example_dirs: + # Test file expected in TESTS_DIR, named e.g. test_.py + test_file = TESTS_DIR / f"test_{example_dir.name}.py" + if not test_file.exists(): + missing_tests.append(example_dir.name) + assert not missing_tests, ( + f"The following example dirs have no test file: {missing_tests}.", + "Test file must be named test_.py.", + ) + + +def test_examples_tmpdir_exists(examples_tmpdir): + """Ensure that the examples temporary directory has been created.""" + # Check the directory itself exists + assert ( + examples_tmpdir.exists() and examples_tmpdir.is_dir() + ), f"Temporary examples directory does not exist: {examples_tmpdir}" diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..51dc57d --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,43 @@ +import importlib.util +import os +from pathlib import Path + + +def load_module_from_path(path: Path): + """Load a module given an absolute Path.""" + spec = importlib.util.spec_from_file_location(path.stem, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def run_cmi_script(script_path: Path): + """Run a script with a main() function.""" + old_cwd = os.getcwd() + os.chdir(script_path.parent) + try: + module = load_module_from_path(script_path) + module.main() + finally: + os.chdir(old_cwd) + + +def run_all_scripts_for_given_example(examples_tmpdir, test_file_path): + """Run all Python scripts in the chapter corresponding to this test + file. + + Only scripts that define a main() function will be executed. + """ + chapter_dir_name = Path(test_file_path).stem.replace("test_", "") + chapter_dir = examples_tmpdir / chapter_dir_name + assert chapter_dir.exists(), f"Chapter dir does not exist: {chapter_dir}" + + # Recursively find all .py scripts + scripts = list(chapter_dir.rglob("*.py")) + for script_path in scripts: + module = load_module_from_path(script_path) + if hasattr(module, "main"): + run_cmi_script(script_path) + else: + # automatically skip helper or non-example scripts + print(f"Skipping file without main(): {script_path.name}") diff --git a/tests/test_ch03NiModelling.py b/tests/test_ch03NiModelling.py new file mode 100644 index 0000000..a52b13f --- /dev/null +++ b/tests/test_ch03NiModelling.py @@ -0,0 +1,5 @@ +from helpers import run_all_scripts_for_given_example + + +def test_scripts(examples_tmpdir): + run_all_scripts_for_given_example(examples_tmpdir, __file__) diff --git a/tests/test_ch05Fit2Phase.py b/tests/test_ch05Fit2Phase.py new file mode 100644 index 0000000..a52b13f --- /dev/null +++ b/tests/test_ch05Fit2Phase.py @@ -0,0 +1,5 @@ +from helpers import run_all_scripts_for_given_example + + +def test_scripts(examples_tmpdir): + run_all_scripts_for_given_example(examples_tmpdir, __file__) diff --git a/tests/test_ch06RefineCrystalStructureGen.py b/tests/test_ch06RefineCrystalStructureGen.py new file mode 100644 index 0000000..a52b13f --- /dev/null +++ b/tests/test_ch06RefineCrystalStructureGen.py @@ -0,0 +1,5 @@ +from helpers import run_all_scripts_for_given_example + + +def test_scripts(examples_tmpdir): + run_all_scripts_for_given_example(examples_tmpdir, __file__) diff --git a/tests/test_ch07StructuralPhaseTransitions.py b/tests/test_ch07StructuralPhaseTransitions.py new file mode 100644 index 0000000..a52b13f --- /dev/null +++ b/tests/test_ch07StructuralPhaseTransitions.py @@ -0,0 +1,5 @@ +from helpers import run_all_scripts_for_given_example + + +def test_scripts(examples_tmpdir): + run_all_scripts_for_given_example(examples_tmpdir, __file__) diff --git a/tests/test_ch08NPRefinement.py b/tests/test_ch08NPRefinement.py new file mode 100644 index 0000000..a52b13f --- /dev/null +++ b/tests/test_ch08NPRefinement.py @@ -0,0 +1,5 @@ +from helpers import run_all_scripts_for_given_example + + +def test_scripts(examples_tmpdir): + run_all_scripts_for_given_example(examples_tmpdir, __file__) diff --git a/tests/test_ch11ClusterXYZ.py b/tests/test_ch11ClusterXYZ.py new file mode 100644 index 0000000..a52b13f --- /dev/null +++ b/tests/test_ch11ClusterXYZ.py @@ -0,0 +1,5 @@ +from helpers import run_all_scripts_for_given_example + + +def test_scripts(examples_tmpdir): + run_all_scripts_for_given_example(examples_tmpdir, __file__)