From 0c5b6600dccf3ede4b85c96cbea9a2a04169a842 Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Wed, 21 May 2025 23:02:32 -0500 Subject: [PATCH 01/14] Switch to new pytest runner --- ...egression_data_test2.txt => frame0001.txt} | 0 ...egression_data_test3.txt => frame0002.txt} | 0 .../test_acoustics_1d_heterogeneous.py | 49 ++----- examples/conftest.py | 10 ++ src/python/classic/test.py | 131 +++++++++++++++--- 5 files changed, 139 insertions(+), 51 deletions(-) rename examples/acoustics_1d_heterogeneous/regression_data/{regression_data_test2.txt => frame0001.txt} (100%) rename examples/acoustics_1d_heterogeneous/regression_data/{regression_data_test3.txt => frame0002.txt} (100%) create mode 100644 examples/conftest.py diff --git a/examples/acoustics_1d_heterogeneous/regression_data/regression_data_test2.txt b/examples/acoustics_1d_heterogeneous/regression_data/frame0001.txt similarity index 100% rename from examples/acoustics_1d_heterogeneous/regression_data/regression_data_test2.txt rename to examples/acoustics_1d_heterogeneous/regression_data/frame0001.txt diff --git a/examples/acoustics_1d_heterogeneous/regression_data/regression_data_test3.txt b/examples/acoustics_1d_heterogeneous/regression_data/frame0002.txt similarity index 100% rename from examples/acoustics_1d_heterogeneous/regression_data/regression_data_test3.txt rename to examples/acoustics_1d_heterogeneous/regression_data/frame0002.txt diff --git a/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py b/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py index dd43352..7882638 100644 --- a/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py +++ b/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py @@ -2,49 +2,28 @@ Regression tests for a 1D heterogeneous acoustics test """ -from __future__ import absolute_import import sys -import unittest +import pytest import clawpack.classic.test as test +def test_acoustics_1d_heterogeneous(tmp_path, save): -class Acoustics1DHeterogeneousTest(test.ClassicRegressionTest): - r"""Basic test for an 1D heterogeneous acoustics test case""" + ctr = test.ClawpackClassicTestRunner(tmp_path, __file__) - def runTest(self, save=False): + ctr.set_data() + ctr.rundata.clawdata.num_output_times = 2 + ctr.rundata.clawdata.tfinal = 5.0 + ctr.rundata.clawdata.output_t0 = False + ctr.write_data() - # Write out data files - self.load_rundata() - - # Modify data for test run - self.rundata.clawdata.num_output_times = 2 - self.rundata.clawdata.tfinal = 5.0 - self.rundata.clawdata.output_t0 = False - self.write_rundata_objects() - - # Run code - self.run_code() - - # Perform tests - self.check_frame(indices=[0, 1], save=save, frame_num=1, - file_name='regression_data_test2.txt') - self.check_frame(indices=[0, 1], save=save, frame_num=2, - file_name='regression_data_test3.txt') - - self.success = True + ctr.executable_name = 'xclaw' + ctr.build_executable() + ctr.run_code() + ctr.check_frame(1, indices=(0, 1), save=save) + ctr.check_frame(2, indices=(0, 1), save=save) if __name__=="__main__": - if len(sys.argv) > 1: - if bool(sys.argv[1]): - # Fake the setup and save out output - test = Acoustics1DHeterogeneousTest() - try: - test.setUp() - test.runTest(save=True) - finally: - test.tearDown() - sys.exit(0) - unittest.main() \ No newline at end of file + pytest.main([__file__]) \ No newline at end of file diff --git a/examples/conftest.py b/examples/conftest.py new file mode 100644 index 0000000..1795428 --- /dev/null +++ b/examples/conftest.py @@ -0,0 +1,10 @@ +"""Configuration for PyTest""" + +import pytest + +def pytest_addoption(parser): + parser.addoption('--save', action='store_true') + +@pytest.fixture +def save(request): + return request.config.getoption("--save", False) diff --git a/src/python/classic/test.py b/src/python/classic/test.py index 21bbae1..3471513 100644 --- a/src/python/classic/test.py +++ b/src/python/classic/test.py @@ -6,32 +6,131 @@ results and looking for errors. """ +from pathlib import Path import os -import glob +import sys +import subprocess +import importlib +import inspect +import shutil +import pytest -import clawpack.clawutil.test -import clawpack.pyclaw.util +import numpy as np + +import clawpack.clawutil.runclaw as runclaw +import clawpack.clawutil.claw_git_status as claw_git_status +import clawpack.pyclaw.solution as solution # Clean library files whenever this module is used if "CLAW" in os.environ: - CLAW = os.environ["CLAW"] + CLAW = Path(os.environ["CLAW"]) else: raise ValueError("Need to set CLAW environment variable.") -for lib_path in [os.path.join(CLAW,"classic","src","1d"), - os.path.join(CLAW,"classic","src","2d"), - os.path.join(CLAW,"classic","src","3d")]: - for path in glob.glob(os.path.join(lib_path,"*.o")): - os.remove(path) - for path in glob.glob(os.path.join(lib_path,"*.mod")): - os.remove(path) +for lib_path in (CLAW / "classic" / "src" / "1d").glob("*.o"): + lib_path.unlink() +for lib_path in (CLAW / "classic" / "src" / "2d").glob("*.o"): + lib_path.unlink() +for lib_path in (CLAW / "classic" / "src" / "2d").glob("*.o"): + lib_path.unlink() + + +class ClawpackClassicTestRunner: + + def __init__(self, path, caller_path): + + self.temp_path = path + # :TODO: see if there's a way to get this automatically + self.test_path = Path(caller_path).parent + self.executable_name = 'xclaw' + + + def set_data(self, setrun_module=None): + + sys.path.insert(0, self.test_path) + if not setrun_module: + setrun_module = 'setrun' + if setrun_module in sys.modules: + del(sys.modules[setrun_module]) + setrun = importlib.import_module(setrun_module) + self.rundata = setrun.setrun() + sys.path.pop(0) + + + def write_data(self, path=None): + + if not path: + path = self.temp_path + self.rundata.write(out_dir=path) + + + def build_executable(self, make_level='default', FFLAGS=None, LFLAGS=None): + + # Assumes GCC CLI + if not FFLAGS: + FFLAGS = os.environ.get('FFLAGS', "-O2 -fopenmp") + if not LFLAGS: + LFLAGS = os.environ.get('LFLAGS', FFLAGS) + + if make_level.lower() == "new": + cmd = "".join((f"cd {self.test_path} ; make new ", + f"FFLAGS='{FFLAGS}' LFLAGS='{LFLAGS}'")) + elif make_level.lower() == "default": + # clean up *.o and *.mod files in test path only + for path in self.test_path.glob("*.o"): + path.unlink() + for path in self.test_path.glob("*.mod"): + path.unlink() + cmd = "".join((f"cd {self.test_path} ; make .exe ", + f"FFLAGS='{FFLAGS}' LFLAGS='{LFLAGS}'")) + + elif make_level.lower() == "exe": + cmd = "".join((f"cd {self.test_path} ; make .exe ", + f"FFLAGS='{FFLAGS}' LFLAGS='{LFLAGS}'")) + else: + raise ValueError(f"Invaled make_level={make_level} given.") + + try: + subprocess.run(cmd, shell=True, check=True) + except subprocess.CalledProcessError as e: + self.clean_up() + raise e + + shutil.move(self.test_path / self.executable_name, self.temp_path) + + + def run_code(self): + runclaw.runclaw(xclawcmd=self.temp_path / self.executable_name, + rundir=self.temp_path, + outdir=self.temp_path, + overwrite=True, + restart=False) + + + def clean_up(self): + pass + + + def check_frame(self, frame, indices=(0), regression_path=None, save=False, **kwargs): + if not(isinstance(indices, tuple) or isinstance(indices, list)): + indices = tuple(indices) -class ClassicRegressionTest(clawpack.clawutil.test.ClawpackRegressionTest): + if not regression_path: + regression_path = self.test_path / "regression_data" - r"""Base Classic regression test setup derived from ClawpackRegressionTest + # Load test output data + sol = solution.Solution(frame, path=self.temp_path) + sol_sums = sol.q[indices, ...].sum(axis=1) - """ + # Load regression data + regression_data = regression_path / f"frame{str(frame).zfill(4)}.txt" + if save: + np.savetxt(regression_data, sol_sums) + claw_git_status.make_git_status_file(outdir=regression_path) + regression_sum = np.loadtxt(regression_data) - __doc__ += clawpack.pyclaw.util.add_parent_doc( - clawpack.clawutil.test.ClawpackRegressionTest) + # Compare data + kwargs.setdefault('rtol', 1e-14) + kwargs.setdefault('atol', 1e-8) + np.testing.assert_allclose(sol_sums, regression_sum, **kwargs) \ No newline at end of file From 5005d712b00b992ddc4e9212c6450bac0e89c8a1 Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Wed, 21 May 2025 23:07:42 -0500 Subject: [PATCH 02/14] Minor adjustment to testing action --- .github/workflows/testing.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 8fd6dde..ee9abd2 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -20,6 +20,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.10" + - name: Install dependencies run: | sudo apt-get update @@ -32,15 +33,16 @@ jobs: with: repository: clawpack/clawpack submodules: true + - name: Checkout classic branch uses: actions/checkout@v4.1.5 with: - repository: clawpack/classic path: classic + - name: Install clawpack run: | - pip install --no-build-isolation --editable $CLAW - # pip install --user -e $CLAW + cd ${CLAW} + pip install --no-build-isolation --editable . - name: Lint with flake8 run: | @@ -49,7 +51,16 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest run: | cd ${CLAW}/classic - pytest + pytest --basetemp=./test_output + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: test_results + path: ${{ env.CLAW }}/geoclaw/*_output + if-no-files-found: ignore From df20f4aed1d6aef0b007d4021e35f3acc7ce8d18 Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Wed, 21 May 2025 23:08:17 -0500 Subject: [PATCH 03/14] Add gauge check --- src/python/classic/test.py | 54 +++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/python/classic/test.py b/src/python/classic/test.py index 3471513..e35529b 100644 --- a/src/python/classic/test.py +++ b/src/python/classic/test.py @@ -133,4 +133,56 @@ def check_frame(self, frame, indices=(0), regression_path=None, save=False, **kw # Compare data kwargs.setdefault('rtol', 1e-14) kwargs.setdefault('atol', 1e-8) - np.testing.assert_allclose(sol_sums, regression_sum, **kwargs) \ No newline at end of file + np.testing.assert_allclose(sol_sums, regression_sum, **kwargs) + + + def check_gauge(self, gauge_id, indices=(0), regression_path=None, save=False, **kwargs): + r"""Basic test to assert gauge equality + + :Input: + - *save* (bool) - If *True* will save the output from this test to + the file *regresion_data.txt*. Default is *False*. + - *indices* (tuple) - Contains indices to compare in the gague + comparison. Defaults to *(0)*. + - *rtol* (float) - Relative tolerance used in the comparison, default + is *1e-14*. Note that the old *tolerance* input is now synonymous + with this parameter. + - *atol* (float) - Absolute tolerance used in the comparison, default + is *1e-08*. + """ + + if not(isinstance(indices, tuple) or isinstance(indices, list)): + indices = tuple(indices) + + if not regression_path: + regression_path = self.test_path / "regression_data" + + # Load test output data + gauge = gauges.GaugeSolution(gauge_id, path=self.temp_path) + + # Load regression data + if save: + shutil.copy(self.temp_path / f"gauge{str(gauge_id).zfill(5)}.txt", + regression_path) + claw_git_status.make_git_status_file(outdir=regression_path) + regression_gauge = gauges.GaugeSolution(gauge_id, path=regression_path) + + # Compare data + kwargs.setdefault('rtol', 1e-14) + kwargs.setdefault('atol', 1e-8) + try: + for n in indices: + np.testing.assert_allclose(gauge.q[n, :], + regression_gauge.q[n, :], + **kwargs) + except AssertionError as e: + err_msg = "\n".join((e.args[0], + "Gauge Match Failed for gauge = %s" % gauge_id)) + err_msg = "\n".join((err_msg, " failures in fields:")) + failure_indices = [] + for n in indices: + if ~np.allclose(gauge.q[n, :], regression_gauge.q[n, :], + **kwargs): + failure_indices.append(str(n)) + index_str = ", ".join(failure_indices) + raise AssertionError(" ".join((err_msg, index_str))) \ No newline at end of file From 548cf236dc8ae6455a801ff49bfcc3db59b048df Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Wed, 21 May 2025 23:22:13 -0500 Subject: [PATCH 04/14] Add missing gauge import --- src/python/classic/test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/python/classic/test.py b/src/python/classic/test.py index e35529b..ec2c8f1 100644 --- a/src/python/classic/test.py +++ b/src/python/classic/test.py @@ -20,6 +20,7 @@ import clawpack.clawutil.runclaw as runclaw import clawpack.clawutil.claw_git_status as claw_git_status import clawpack.pyclaw.solution as solution +import clawpack.pyclaw.gauges as gauges # Clean library files whenever this module is used if "CLAW" in os.environ: From 7f1d8ab0b59742668dec1a170c35c9ff1e133907 Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Wed, 21 May 2025 23:41:02 -0500 Subject: [PATCH 05/14] Convert 2d annulus test --- ...egression_data_test2.txt => frame0001.txt} | 0 ...egression_data_test3.txt => frame0002.txt} | 0 .../test_advection_2d_annulus.py | 48 +++++-------------- src/python/classic/test.py | 4 +- 4 files changed, 15 insertions(+), 37 deletions(-) rename examples/advection_2d_annulus/regression_data/{regression_data_test2.txt => frame0001.txt} (100%) rename examples/advection_2d_annulus/regression_data/{regression_data_test3.txt => frame0002.txt} (100%) diff --git a/examples/advection_2d_annulus/regression_data/regression_data_test2.txt b/examples/advection_2d_annulus/regression_data/frame0001.txt similarity index 100% rename from examples/advection_2d_annulus/regression_data/regression_data_test2.txt rename to examples/advection_2d_annulus/regression_data/frame0001.txt diff --git a/examples/advection_2d_annulus/regression_data/regression_data_test3.txt b/examples/advection_2d_annulus/regression_data/frame0002.txt similarity index 100% rename from examples/advection_2d_annulus/regression_data/regression_data_test3.txt rename to examples/advection_2d_annulus/regression_data/frame0002.txt diff --git a/examples/advection_2d_annulus/test_advection_2d_annulus.py b/examples/advection_2d_annulus/test_advection_2d_annulus.py index 8edde85..17a618f 100644 --- a/examples/advection_2d_annulus/test_advection_2d_annulus.py +++ b/examples/advection_2d_annulus/test_advection_2d_annulus.py @@ -2,49 +2,27 @@ Regression tests for 2D advection on an annulus. """ -from __future__ import absolute_import import sys -import unittest +import pytest import clawpack.classic.test as test +def test_advection_2d_annulus(tmp_path, save): -class Advection2DAnnulusTest(test.ClassicRegressionTest): - r"""Basic test for a 2D advection test case""" + ctr = test.ClawpackClassicTestRunner(tmp_path, __file__) + ctr.set_data() + ctr.rundata.clawdata.num_output_times = 2 + ctr.rundata.clawdata.tfinal = 0.5 + ctr.write_data() - def runTest(self, save=False): - - # Write out data files - self.load_rundata() - - self.rundata.clawdata.num_output_times = 2 - self.rundata.clawdata.tfinal = 0.5 - - self.write_rundata_objects() - - # Run code - self.run_code() - - # Perform tests - self.check_frame(save=save, frame_num=1, - file_name='regression_data_test2.txt') - self.check_frame(save=save, frame_num=2, - file_name='regression_data_test3.txt') - - self.success = True + ctr.executable_name = 'xclaw' + ctr.build_executable() + ctr.run_code() + ctr.check_frame(1, save=save) + ctr.check_frame(2, save=save) if __name__=="__main__": - if len(sys.argv) > 1: - if bool(sys.argv[1]): - # Fake the setup and save out output - test = Advection2DAnnulusTest() - try: - test.setUp() - test.runTest(save=True) - finally: - test.tearDown() - sys.exit(0) - unittest.main() \ No newline at end of file + pytest.main([__file__]) \ No newline at end of file diff --git a/src/python/classic/test.py b/src/python/classic/test.py index ec2c8f1..5a4ca37 100644 --- a/src/python/classic/test.py +++ b/src/python/classic/test.py @@ -112,7 +112,7 @@ def clean_up(self): pass - def check_frame(self, frame, indices=(0), regression_path=None, save=False, **kwargs): + def check_frame(self, frame, indices=(0,), regression_path=None, save=False, **kwargs): if not(isinstance(indices, tuple) or isinstance(indices, list)): indices = tuple(indices) @@ -122,7 +122,7 @@ def check_frame(self, frame, indices=(0), regression_path=None, save=False, **kw # Load test output data sol = solution.Solution(frame, path=self.temp_path) - sol_sums = sol.q[indices, ...].sum(axis=1) + sol_sums = [sol.q[i, ...].sum() for i in indices] # Load regression data regression_data = regression_path / f"frame{str(frame).zfill(4)}.txt" From c3910b50154278acccf65fc49306b03759738a9c Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Wed, 21 May 2025 23:45:36 -0500 Subject: [PATCH 06/14] Convert 3d heterogeneous test --- ...egression_data_test2.txt => frame0001.txt} | 0 ...egression_data_test3.txt => frame0002.txt} | 0 .../test_acoustics_3d_heterogeneous.py | 45 ++++++------------- 3 files changed, 13 insertions(+), 32 deletions(-) rename examples/acoustics_3d_heterogeneous/regression_data/{regression_data_test2.txt => frame0001.txt} (100%) rename examples/acoustics_3d_heterogeneous/regression_data/{regression_data_test3.txt => frame0002.txt} (100%) diff --git a/examples/acoustics_3d_heterogeneous/regression_data/regression_data_test2.txt b/examples/acoustics_3d_heterogeneous/regression_data/frame0001.txt similarity index 100% rename from examples/acoustics_3d_heterogeneous/regression_data/regression_data_test2.txt rename to examples/acoustics_3d_heterogeneous/regression_data/frame0001.txt diff --git a/examples/acoustics_3d_heterogeneous/regression_data/regression_data_test3.txt b/examples/acoustics_3d_heterogeneous/regression_data/frame0002.txt similarity index 100% rename from examples/acoustics_3d_heterogeneous/regression_data/regression_data_test3.txt rename to examples/acoustics_3d_heterogeneous/regression_data/frame0002.txt diff --git a/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py b/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py index 4409ca0..3c977fb 100644 --- a/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py +++ b/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py @@ -8,43 +8,24 @@ import clawpack.classic.test as test -class Acoustics3DHeterogeneousTest(test.ClassicRegressionTest): +def test_acoustics_3d_heterogeneous(tmp_path, save): r"""Basic test for a 3D heterogeneous acoustics test.""" + ctr = test.ClawpackClassicTestRunner(tmp_path, __file__) - def runTest(self, save=False): + ctr.set_data() + ctr.rundata.clawdata.num_cells = [20, 20, 20] + ctr.rundata.clawdata.num_output_times = 2 + ctr.rundata.clawdata.tfinal = 1.0 + ctr.write_data() - # Write out data files - self.load_rundata() - - self.rundata.clawdata.num_cells = [20, 20, 20] - self.rundata.clawdata.num_output_times = 2 - self.rundata.clawdata.tfinal = 1.0 - - self.write_rundata_objects() - - # Run code - self.run_code() - - # Perform tests - self.check_frame(save=save, indices=[0, 1, 2], frame_num=1, - file_name='regression_data_test2.txt') - self.check_frame(save=save, indices=[0, 1, 2], frame_num=2, - file_name='regression_data_test3.txt') - - self.success = True + ctr.executable_name = 'xclaw' + ctr.build_executable() + ctr.run_code() + ctr.check_frame(1, indices=(0, 1, 2), save=save) + ctr.check_frame(2, indices=(0, 1, 2), save=save) if __name__=="__main__": - if len(sys.argv) > 1: - if bool(sys.argv[1]): - # Fake the setup and save out output - test = Acoustics3DHeterogeneousTest() - try: - test.setUp() - test.runTest(save=True) - finally: - test.tearDown() - sys.exit(0) - unittest.main() \ No newline at end of file + pytest.main([__file__]) \ No newline at end of file From 907b6cec37d43980f2ab9dc9362d92ab5c02742b Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Mon, 28 Jul 2025 12:13:57 -0400 Subject: [PATCH 07/14] Multiple test in one run now working --- examples/acoustics_1d_heterogeneous/setrun.py | 1 - .../test_acoustics_1d_heterogeneous.py | 9 ++-- .../test_acoustics_3d_heterogeneous.py | 12 ++--- .../test_advection_2d_annulus.py | 9 ++-- src/python/classic/test.py | 54 ++++++++++--------- 5 files changed, 43 insertions(+), 42 deletions(-) diff --git a/examples/acoustics_1d_heterogeneous/setrun.py b/examples/acoustics_1d_heterogeneous/setrun.py index c0c4bc4..02b7c07 100644 --- a/examples/acoustics_1d_heterogeneous/setrun.py +++ b/examples/acoustics_1d_heterogeneous/setrun.py @@ -6,7 +6,6 @@ """ -from __future__ import absolute_import import os import numpy as np diff --git a/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py b/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py index 7882638..7c45783 100644 --- a/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py +++ b/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py @@ -2,14 +2,13 @@ Regression tests for a 1D heterogeneous acoustics test """ -import sys -import pytest +from pathlib import Path import clawpack.classic.test as test -def test_acoustics_1d_heterogeneous(tmp_path, save): +def test_acoustics_1d_heterogeneous(tmp_path: Path, save: bool): - ctr = test.ClawpackClassicTestRunner(tmp_path, __file__) + ctr = test.ClawpackClassicTestRunner(tmp_path) ctr.set_data() ctr.rundata.clawdata.num_output_times = 2 @@ -26,4 +25,4 @@ def test_acoustics_1d_heterogeneous(tmp_path, save): ctr.check_frame(2, indices=(0, 1), save=save) if __name__=="__main__": - pytest.main([__file__]) \ No newline at end of file + pytest.main([__file__]) diff --git a/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py b/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py index 3c977fb..9db4449 100644 --- a/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py +++ b/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py @@ -2,16 +2,14 @@ Regression tests for 3D heterogeneous acoustics problem. """ -import sys -import unittest +from pathlib import Path import clawpack.classic.test as test - -def test_acoustics_3d_heterogeneous(tmp_path, save): +def test_acoustics_3d_heterogeneous(tmp_path: Path, save: bool): r"""Basic test for a 3D heterogeneous acoustics test.""" - ctr = test.ClawpackClassicTestRunner(tmp_path, __file__) + ctr = test.ClawpackClassicTestRunner(tmp_path) ctr.set_data() ctr.rundata.clawdata.num_cells = [20, 20, 20] @@ -27,5 +25,5 @@ def test_acoustics_3d_heterogeneous(tmp_path, save): ctr.check_frame(1, indices=(0, 1, 2), save=save) ctr.check_frame(2, indices=(0, 1, 2), save=save) -if __name__=="__main__": - pytest.main([__file__]) \ No newline at end of file +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/examples/advection_2d_annulus/test_advection_2d_annulus.py b/examples/advection_2d_annulus/test_advection_2d_annulus.py index 17a618f..70cd2e1 100644 --- a/examples/advection_2d_annulus/test_advection_2d_annulus.py +++ b/examples/advection_2d_annulus/test_advection_2d_annulus.py @@ -2,14 +2,13 @@ Regression tests for 2D advection on an annulus. """ -import sys -import pytest +from pathlib import Path import clawpack.classic.test as test -def test_advection_2d_annulus(tmp_path, save): +def test_advection_2d_annulus(tmp_path: Path, save: bool): - ctr = test.ClawpackClassicTestRunner(tmp_path, __file__) + ctr = test.ClawpackClassicTestRunner(tmp_path) ctr.set_data() ctr.rundata.clawdata.num_output_times = 2 @@ -25,4 +24,4 @@ def test_advection_2d_annulus(tmp_path, save): ctr.check_frame(2, save=save) if __name__=="__main__": - pytest.main([__file__]) \ No newline at end of file + pytest.main([__file__]) diff --git a/src/python/classic/test.py b/src/python/classic/test.py index 5a4ca37..a284203 100644 --- a/src/python/classic/test.py +++ b/src/python/classic/test.py @@ -11,9 +11,10 @@ import sys import subprocess import importlib -import inspect import shutil -import pytest +import inspect +import random +import string import numpy as np @@ -32,30 +33,32 @@ lib_path.unlink() for lib_path in (CLAW / "classic" / "src" / "2d").glob("*.o"): lib_path.unlink() -for lib_path in (CLAW / "classic" / "src" / "2d").glob("*.o"): +for lib_path in (CLAW / "classic" / "src" / "3d").glob("*.o"): lib_path.unlink() - class ClawpackClassicTestRunner: - def __init__(self, path, caller_path): + def __init__(self, path: Path): self.temp_path = path - # :TODO: see if there's a way to get this automatically - self.test_path = Path(caller_path).parent + self.test_path = Path(Path(inspect.stack()[1].filename).absolute()).parent self.executable_name = 'xclaw' - def set_data(self, setrun_module=None): + def set_data(self, setrun_path: Path = None): + r"""Set the rundata for the test.""" + + if not setrun_path: + setrun_path = self.test_path / "setrun.py" - sys.path.insert(0, self.test_path) - if not setrun_module: - setrun_module = 'setrun' - if setrun_module in sys.modules: - del(sys.modules[setrun_module]) - setrun = importlib.import_module(setrun_module) - self.rundata = setrun.setrun() - sys.path.pop(0) + mod_name = '_'.join(("setrun", + "".join(random.choices(string.ascii_letters + + string.digits, k=32)))) + spec = importlib.util.spec_from_file_location(mod_name, setrun_path) + setrun_module = importlib.util.module_from_spec(spec) + sys.modules[mod_name] = setrun_module + spec.loader.exec_module(setrun_module) + self.rundata = setrun_module.setrun() def write_data(self, path=None): @@ -101,6 +104,9 @@ def build_executable(self, make_level='default', FFLAGS=None, LFLAGS=None): def run_code(self): + print(f"clawcmd={self.temp_path / self.executable_name}") + print(f"rundir={self.temp_path}") + print(f"outdir={self.temp_path}") runclaw.runclaw(xclawcmd=self.temp_path / self.executable_name, rundir=self.temp_path, outdir=self.temp_path, @@ -141,12 +147,12 @@ def check_gauge(self, gauge_id, indices=(0), regression_path=None, save=False, * r"""Basic test to assert gauge equality :Input: - - *save* (bool) - If *True* will save the output from this test to + - *save* (bool) - If *True* will save the output from this test to the file *regresion_data.txt*. Default is *False*. - - *indices* (tuple) - Contains indices to compare in the gague + - *indices* (tuple) - Contains indices to compare in the gague comparison. Defaults to *(0)*. - - *rtol* (float) - Relative tolerance used in the comparison, default - is *1e-14*. Note that the old *tolerance* input is now synonymous + - *rtol* (float) - Relative tolerance used in the comparison, default + is *1e-14*. Note that the old *tolerance* input is now synonymous with this parameter. - *atol* (float) - Absolute tolerance used in the comparison, default is *1e-08*. @@ -174,16 +180,16 @@ def check_gauge(self, gauge_id, indices=(0), regression_path=None, save=False, * try: for n in indices: np.testing.assert_allclose(gauge.q[n, :], - regression_gauge.q[n, :], + regression_gauge.q[n, :], **kwargs) except AssertionError as e: - err_msg = "\n".join((e.args[0], + err_msg = "\n".join((e.args[0], "Gauge Match Failed for gauge = %s" % gauge_id)) err_msg = "\n".join((err_msg, " failures in fields:")) failure_indices = [] for n in indices: - if ~np.allclose(gauge.q[n, :], regression_gauge.q[n, :], + if ~np.allclose(gauge.q[n, :], regression_gauge.q[n, :], **kwargs): failure_indices.append(str(n)) index_str = ", ".join(failure_indices) - raise AssertionError(" ".join((err_msg, index_str))) \ No newline at end of file + raise AssertionError(" ".join((err_msg, index_str))) From 00b635d82d6482594461ff1c29d5c7c9f8406560 Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Mon, 28 Jul 2025 12:17:43 -0400 Subject: [PATCH 08/14] Add pytest import --- .../test_acoustics_1d_heterogeneous.py | 2 +- .../test_acoustics_3d_heterogeneous.py | 2 +- examples/advection_2d_annulus/test_advection_2d_annulus.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py b/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py index 7c45783..d6cfb38 100644 --- a/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py +++ b/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py @@ -3,7 +3,7 @@ """ from pathlib import Path - +import pytest import clawpack.classic.test as test def test_acoustics_1d_heterogeneous(tmp_path: Path, save: bool): diff --git a/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py b/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py index 9db4449..c2c89a6 100644 --- a/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py +++ b/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py @@ -3,7 +3,7 @@ """ from pathlib import Path - +import pytest import clawpack.classic.test as test def test_acoustics_3d_heterogeneous(tmp_path: Path, save: bool): diff --git a/examples/advection_2d_annulus/test_advection_2d_annulus.py b/examples/advection_2d_annulus/test_advection_2d_annulus.py index 70cd2e1..d1e91a4 100644 --- a/examples/advection_2d_annulus/test_advection_2d_annulus.py +++ b/examples/advection_2d_annulus/test_advection_2d_annulus.py @@ -3,7 +3,7 @@ """ from pathlib import Path - +import pytest import clawpack.classic.test as test def test_advection_2d_annulus(tmp_path: Path, save: bool): From 4b60fe088a56277862f3afbc42f48070ad9bbe79 Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Mon, 28 Jul 2025 12:23:37 -0400 Subject: [PATCH 09/14] Always upload artifacts --- .github/workflows/testing.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index ee9abd2..71a1c5a 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -58,9 +58,10 @@ jobs: pytest --basetemp=./test_output - name: Upload test results - if: failure() + # if: failure() - Always upload artifacts now uses: actions/upload-artifact@v4 with: name: test_results - path: ${{ env.CLAW }}/geoclaw/*_output + path: ./test_output + # path: ${{ env.CLAW }}/geoclaw/*_output if-no-files-found: ignore From 737368a1ffa2922bc05054e1be96ebe9c58185ed Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Mon, 28 Jul 2025 12:34:21 -0400 Subject: [PATCH 10/14] Change test artifact path to absolute --- .github/workflows/testing.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 71a1c5a..fdd1ba0 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -62,6 +62,5 @@ jobs: uses: actions/upload-artifact@v4 with: name: test_results - path: ./test_output - # path: ${{ env.CLAW }}/geoclaw/*_output + path: ${{ env.CLAW }}/classic/test_output if-no-files-found: ignore From 220dfca0709073cada9662158e516f8ca63e6707 Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Mon, 28 Jul 2025 12:35:40 -0400 Subject: [PATCH 11/14] Change name of action to test --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index fdd1ba0..64a97f6 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -13,7 +13,7 @@ env: CLAW: ${{ github.workspace }} jobs: - build: + test: runs-on: ubuntu-latest steps: - name: Set up Python 3.10 From 1fc859cbc0a441582c8747c84fbef69502cd7021 Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Fri, 12 Sep 2025 10:20:11 -0400 Subject: [PATCH 12/14] Move new test runner to clawutil --- src/python/classic/test.py | 171 +------------------------------------ 1 file changed, 3 insertions(+), 168 deletions(-) diff --git a/src/python/classic/test.py b/src/python/classic/test.py index a284203..410fd3d 100644 --- a/src/python/classic/test.py +++ b/src/python/classic/test.py @@ -8,20 +8,8 @@ from pathlib import Path import os -import sys -import subprocess -import importlib -import shutil -import inspect -import random -import string -import numpy as np - -import clawpack.clawutil.runclaw as runclaw -import clawpack.clawutil.claw_git_status as claw_git_status -import clawpack.pyclaw.solution as solution -import clawpack.pyclaw.gauges as gauges +import clawpack.clawutil.test as test # Clean library files whenever this module is used if "CLAW" in os.environ: @@ -36,160 +24,7 @@ for lib_path in (CLAW / "classic" / "src" / "3d").glob("*.o"): lib_path.unlink() -class ClawpackClassicTestRunner: +class ClawpackClassicTestRunner(test.ClawpackTestRunner): def __init__(self, path: Path): - - self.temp_path = path - self.test_path = Path(Path(inspect.stack()[1].filename).absolute()).parent - self.executable_name = 'xclaw' - - - def set_data(self, setrun_path: Path = None): - r"""Set the rundata for the test.""" - - if not setrun_path: - setrun_path = self.test_path / "setrun.py" - - mod_name = '_'.join(("setrun", - "".join(random.choices(string.ascii_letters - + string.digits, k=32)))) - spec = importlib.util.spec_from_file_location(mod_name, setrun_path) - setrun_module = importlib.util.module_from_spec(spec) - sys.modules[mod_name] = setrun_module - spec.loader.exec_module(setrun_module) - self.rundata = setrun_module.setrun() - - - def write_data(self, path=None): - - if not path: - path = self.temp_path - self.rundata.write(out_dir=path) - - - def build_executable(self, make_level='default', FFLAGS=None, LFLAGS=None): - - # Assumes GCC CLI - if not FFLAGS: - FFLAGS = os.environ.get('FFLAGS', "-O2 -fopenmp") - if not LFLAGS: - LFLAGS = os.environ.get('LFLAGS', FFLAGS) - - if make_level.lower() == "new": - cmd = "".join((f"cd {self.test_path} ; make new ", - f"FFLAGS='{FFLAGS}' LFLAGS='{LFLAGS}'")) - elif make_level.lower() == "default": - # clean up *.o and *.mod files in test path only - for path in self.test_path.glob("*.o"): - path.unlink() - for path in self.test_path.glob("*.mod"): - path.unlink() - cmd = "".join((f"cd {self.test_path} ; make .exe ", - f"FFLAGS='{FFLAGS}' LFLAGS='{LFLAGS}'")) - - elif make_level.lower() == "exe": - cmd = "".join((f"cd {self.test_path} ; make .exe ", - f"FFLAGS='{FFLAGS}' LFLAGS='{LFLAGS}'")) - else: - raise ValueError(f"Invaled make_level={make_level} given.") - - try: - subprocess.run(cmd, shell=True, check=True) - except subprocess.CalledProcessError as e: - self.clean_up() - raise e - - shutil.move(self.test_path / self.executable_name, self.temp_path) - - - def run_code(self): - print(f"clawcmd={self.temp_path / self.executable_name}") - print(f"rundir={self.temp_path}") - print(f"outdir={self.temp_path}") - runclaw.runclaw(xclawcmd=self.temp_path / self.executable_name, - rundir=self.temp_path, - outdir=self.temp_path, - overwrite=True, - restart=False) - - - def clean_up(self): - pass - - - def check_frame(self, frame, indices=(0,), regression_path=None, save=False, **kwargs): - - if not(isinstance(indices, tuple) or isinstance(indices, list)): - indices = tuple(indices) - - if not regression_path: - regression_path = self.test_path / "regression_data" - - # Load test output data - sol = solution.Solution(frame, path=self.temp_path) - sol_sums = [sol.q[i, ...].sum() for i in indices] - - # Load regression data - regression_data = regression_path / f"frame{str(frame).zfill(4)}.txt" - if save: - np.savetxt(regression_data, sol_sums) - claw_git_status.make_git_status_file(outdir=regression_path) - regression_sum = np.loadtxt(regression_data) - - # Compare data - kwargs.setdefault('rtol', 1e-14) - kwargs.setdefault('atol', 1e-8) - np.testing.assert_allclose(sol_sums, regression_sum, **kwargs) - - - def check_gauge(self, gauge_id, indices=(0), regression_path=None, save=False, **kwargs): - r"""Basic test to assert gauge equality - - :Input: - - *save* (bool) - If *True* will save the output from this test to - the file *regresion_data.txt*. Default is *False*. - - *indices* (tuple) - Contains indices to compare in the gague - comparison. Defaults to *(0)*. - - *rtol* (float) - Relative tolerance used in the comparison, default - is *1e-14*. Note that the old *tolerance* input is now synonymous - with this parameter. - - *atol* (float) - Absolute tolerance used in the comparison, default - is *1e-08*. - """ - - if not(isinstance(indices, tuple) or isinstance(indices, list)): - indices = tuple(indices) - - if not regression_path: - regression_path = self.test_path / "regression_data" - - # Load test output data - gauge = gauges.GaugeSolution(gauge_id, path=self.temp_path) - - # Load regression data - if save: - shutil.copy(self.temp_path / f"gauge{str(gauge_id).zfill(5)}.txt", - regression_path) - claw_git_status.make_git_status_file(outdir=regression_path) - regression_gauge = gauges.GaugeSolution(gauge_id, path=regression_path) - - # Compare data - kwargs.setdefault('rtol', 1e-14) - kwargs.setdefault('atol', 1e-8) - try: - for n in indices: - np.testing.assert_allclose(gauge.q[n, :], - regression_gauge.q[n, :], - **kwargs) - except AssertionError as e: - err_msg = "\n".join((e.args[0], - "Gauge Match Failed for gauge = %s" % gauge_id)) - err_msg = "\n".join((err_msg, " failures in fields:")) - failure_indices = [] - for n in indices: - if ~np.allclose(gauge.q[n, :], regression_gauge.q[n, :], - **kwargs): - failure_indices.append(str(n)) - index_str = ", ".join(failure_indices) - raise AssertionError(" ".join((err_msg, index_str))) + super(ClawpackClassicTestRunner, self).__init__(path) From c15a1d58d4358435b5d6e23ceec08bfd461c99aa Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Fri, 12 Sep 2025 10:48:26 -0400 Subject: [PATCH 13/14] Cleanup names and minor bugs --- .../test_acoustics_1d_heterogeneous.py | 2 +- .../test_acoustics_3d_heterogeneous.py | 2 +- .../advection_2d_annulus/test_advection_2d_annulus.py | 2 +- src/python/classic/test.py | 11 +++++------ 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py b/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py index d6cfb38..c20a187 100644 --- a/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py +++ b/examples/acoustics_1d_heterogeneous/test_acoustics_1d_heterogeneous.py @@ -8,7 +8,7 @@ def test_acoustics_1d_heterogeneous(tmp_path: Path, save: bool): - ctr = test.ClawpackClassicTestRunner(tmp_path) + ctr = test.ClassicTestRunner(tmp_path) ctr.set_data() ctr.rundata.clawdata.num_output_times = 2 diff --git a/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py b/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py index c2c89a6..cd62911 100644 --- a/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py +++ b/examples/acoustics_3d_heterogeneous/test_acoustics_3d_heterogeneous.py @@ -9,7 +9,7 @@ def test_acoustics_3d_heterogeneous(tmp_path: Path, save: bool): r"""Basic test for a 3D heterogeneous acoustics test.""" - ctr = test.ClawpackClassicTestRunner(tmp_path) + ctr = test.ClassicTestRunner(tmp_path) ctr.set_data() ctr.rundata.clawdata.num_cells = [20, 20, 20] diff --git a/examples/advection_2d_annulus/test_advection_2d_annulus.py b/examples/advection_2d_annulus/test_advection_2d_annulus.py index d1e91a4..49a8191 100644 --- a/examples/advection_2d_annulus/test_advection_2d_annulus.py +++ b/examples/advection_2d_annulus/test_advection_2d_annulus.py @@ -8,7 +8,7 @@ def test_advection_2d_annulus(tmp_path: Path, save: bool): - ctr = test.ClawpackClassicTestRunner(tmp_path) + ctr = test.ClassicTestRunner(tmp_path) ctr.set_data() ctr.rundata.clawdata.num_output_times = 2 diff --git a/src/python/classic/test.py b/src/python/classic/test.py index 410fd3d..e233d66 100644 --- a/src/python/classic/test.py +++ b/src/python/classic/test.py @@ -1,9 +1,8 @@ r""" -Execute nosetests in all subdirectories, to run a series of quick -regression tests. +Defines the Classic Clawpack Test Runner class for running PyTest based +regression tests in classic clawpack. -Sends output and result/errors to separate files to simplify checking -results and looking for errors. +Refer to the documentation for PyTest to manage output and reporting. """ from pathlib import Path @@ -24,7 +23,7 @@ for lib_path in (CLAW / "classic" / "src" / "3d").glob("*.o"): lib_path.unlink() -class ClawpackClassicTestRunner(test.ClawpackTestRunner): +class ClassicTestRunner(test.ClawpackTestRunner): def __init__(self, path: Path): - super(ClawpackClassicTestRunner, self).__init__(path) + super(ClassicTestRunner, self).__init__(path) From 86b888692bcac431d7d9a42953af88e66d96f57e Mon Sep 17 00:00:00 2001 From: Kyle Mandli Date: Mon, 15 Sep 2025 11:54:53 -0400 Subject: [PATCH 14/14] Add test_path input option --- src/python/classic/test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/python/classic/test.py b/src/python/classic/test.py index e233d66..c037c75 100644 --- a/src/python/classic/test.py +++ b/src/python/classic/test.py @@ -7,6 +7,7 @@ from pathlib import Path import os +from typing import Optional import clawpack.clawutil.test as test @@ -25,5 +26,5 @@ class ClassicTestRunner(test.ClawpackTestRunner): - def __init__(self, path: Path): - super(ClassicTestRunner, self).__init__(path) + def __init__(self, path: Path, test_path: Optional[Path]=None): + super(ClassicTestRunner, self).__init__(path, test_path=test_path)