model-config-tests 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/PKG-INFO +1 -1
  2. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/pyproject.toml +3 -1
  3. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/_version.py +3 -3
  4. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/config_tests/qa/test_access_om2_config.py +1 -1
  5. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/config_tests/test_bit_reproducibility.py +33 -26
  6. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/exp_test_helper.py +42 -16
  7. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests.egg-info/PKG-INFO +1 -1
  8. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/tests/test_exp_test_helper.py +72 -6
  9. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/LICENSE +0 -0
  10. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/README.md +0 -0
  11. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/setup.cfg +0 -0
  12. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/setup.py +0 -0
  13. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/__init__.py +0 -0
  14. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/cmds/compare_exp_tests_cmd.py +0 -0
  15. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/cmds/config_tests_cmd.py +0 -0
  16. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/compare_exp_tests/conftest.py +0 -0
  17. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/compare_exp_tests/test_repro.py +0 -0
  18. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/config_tests/conftest.py +0 -0
  19. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/config_tests/qa/test_access_esm1p5_config.py +0 -0
  20. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/config_tests/qa/test_access_esm1p6_config.py +0 -0
  21. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/config_tests/qa/test_access_om3_config.py +0 -0
  22. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/config_tests/qa/test_config.py +0 -0
  23. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/models/__init__.py +0 -0
  24. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/models/accessesm1p5.py +0 -0
  25. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/models/accessesm1p6.py +0 -0
  26. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/models/accessom2.py +0 -0
  27. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/models/accessom3.py +0 -0
  28. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/models/model.py +0 -0
  29. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/models/mom5.py +0 -0
  30. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/models/um7.py +0 -0
  31. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests/util.py +0 -0
  32. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests.egg-info/SOURCES.txt +0 -0
  33. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests.egg-info/dependency_links.txt +0 -0
  34. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests.egg-info/entry_points.txt +0 -0
  35. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests.egg-info/requires.txt +0 -0
  36. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/src/model_config_tests.egg-info/top_level.txt +0 -0
  37. {model_config_tests-0.2.2 → model_config_tests-0.2.3}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model_config_tests
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Test for ACCESS model (payu) configurations
5
5
  Author: ACCESS-NRI
6
6
  License: Apache-2.0
@@ -112,6 +112,8 @@ tag_prefix = "v"
112
112
  parentdir_prefix = "model_config_tests-"
113
113
 
114
114
  [tool.coverage.run]
115
+ patch = ["subprocess"]
115
116
  omit = [
116
- "src/model_config_tests/_version.py"
117
+ "*/model_config_tests/_version.py",
118
+ "src/model_config_tests/_version.py",
117
119
  ]
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-10-21T07:48:35+1100",
11
+ "date": "2025-12-19T17:08:47+1100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "5b34e0c7b8cb513b5204a8baa5f97b22f84c2407",
15
- "version": "0.2.2"
14
+ "full-revisionid": "31685c167c305b26642a22f00796dec7d8725551",
15
+ "version": "0.2.3"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -116,7 +116,7 @@ class TestAccessOM2:
116
116
 
117
117
  def test_mppncombine_fast_collate_exe(self, config, branch):
118
118
  if branch.is_high_resolution:
119
- pattern = r"/g/data/vk83/apps/mppnccombine-fast/.*/bin/mppnccombine-fast"
119
+ pattern = r".*mppnccombine-fast"
120
120
  if "collate" in config:
121
121
  assert re.match(
122
122
  pattern, config["collate"]["exe"]
@@ -9,7 +9,7 @@ from typing import Optional
9
9
 
10
10
  import pytest
11
11
 
12
- from model_config_tests.exp_test_helper import Experiments
12
+ from model_config_tests.exp_test_helper import Experiments, ExpTestHelper
13
13
  from model_config_tests.util import DAY_IN_SECONDS, HOUR_IN_SECONDS
14
14
 
15
15
  # Names of shared experiments
@@ -147,6 +147,20 @@ def experiments(
147
147
  return _experiments(experiments_markers, output_path, control_path, keep_archive)
148
148
 
149
149
 
150
+ @pytest.fixture
151
+ def requested_experiments(request, experiments: Experiments):
152
+ """Fixture to check that requested experiments have run successfully
153
+ and return a dictionary of ExpTestHelper instances for each experiment."""
154
+ exp_marker = request.node.get_closest_marker("experiments").args[0]
155
+ requested_exps = {}
156
+ for exp_name in exp_marker:
157
+ # Check experiment has run successfully - this will raise an
158
+ # error if there are any non-zero exit codes in the outputs
159
+ experiments.check_experiment(exp_name)
160
+ requested_exps[exp_name] = experiments.get_experiment(exp_name)
161
+ return requested_exps
162
+
163
+
150
164
  class TestBitReproducibility:
151
165
 
152
166
  @pytest.mark.repro
@@ -160,7 +174,7 @@ class TestBitReproducibility:
160
174
  self,
161
175
  output_path: Path,
162
176
  control_path: Path,
163
- experiments: Experiments,
177
+ requested_experiments: dict[str, ExpTestHelper],
164
178
  checksum_path: Optional[Path],
165
179
  ):
166
180
  """
@@ -178,9 +192,9 @@ class TestBitReproducibility:
178
192
  Path to the model configuration to test. This is copied for
179
193
  for control directories in experiments. Default is set in
180
194
  conftests.py.
181
- experiments: Experiments
182
- Class that manages the shared experiments. This is a fixture
183
- defined in this file.
195
+ requested_experiments: dict[str, ExpTestHelper]
196
+ A dictionary of requested experiments, where the key is the
197
+ experiment name and the value is an instance of ExpTestHelper.
184
198
  checksum_path: Optional[Path]
185
199
  Path to checksums to compare model output against. Default is
186
200
  set to checksums saved on model configuration. This is a
@@ -190,12 +204,7 @@ class TestBitReproducibility:
190
204
  checksum_output_dir = set_checksum_output_dir(output_path=output_path)
191
205
 
192
206
  # Use default runtime experiment to get the historical checksums
193
- experiments.check_experiments([EXP_DEFAULT_RUNTIME])
194
- exp = experiments.get_experiment(EXP_DEFAULT_RUNTIME)
195
-
196
- assert (
197
- exp.model.output_exists()
198
- ), "Output file required for model checksums does not exist"
207
+ exp = requested_experiments.get(EXP_DEFAULT_RUNTIME)
199
208
 
200
209
  # Set the checksum output filename using the model default runtime
201
210
  runtime_hours = exp.model.default_runtime_seconds // HOUR_IN_SECONDS
@@ -235,20 +244,16 @@ class TestBitReproducibility:
235
244
  EXP_1D_RUNTIME_REPEAT: {"n_runs": 1, "model_runtime": DAY_IN_SECONDS},
236
245
  }
237
246
  )
238
- def test_repro_determinism(self, experiments: Experiments):
247
+ def test_repro_determinism(self, requested_experiments: dict[str, ExpTestHelper]):
239
248
  """
240
249
  Determinism test that confirms repeated model runs for 1 day
241
250
  give the same results
242
251
  """
243
- experiments.check_experiments([EXP_1D_RUNTIME, EXP_1D_RUNTIME_REPEAT])
244
- exp_1d_runtime = experiments.get_experiment(EXP_1D_RUNTIME)
245
- exp_1d_runtime_repeat = experiments.get_experiment(EXP_1D_RUNTIME_REPEAT)
252
+ exp_1d_runtime = requested_experiments.get(EXP_1D_RUNTIME)
253
+ exp_1d_runtime_repeat = requested_experiments.get(EXP_1D_RUNTIME_REPEAT)
246
254
 
247
255
  # Compare expected to produced.
248
- assert exp_1d_runtime.model.output_exists()
249
256
  expected = exp_1d_runtime.extract_checksums()
250
-
251
- assert exp_1d_runtime_repeat.model.output_exists()
252
257
  produced = exp_1d_runtime_repeat.extract_checksums()
253
258
 
254
259
  assert produced == expected
@@ -262,16 +267,17 @@ class TestBitReproducibility:
262
267
  EXP_2D_RUNTIME: {"n_runs": 1, "model_runtime": 2 * DAY_IN_SECONDS},
263
268
  }
264
269
  )
265
- def test_repro_restart(self, output_path: Path, experiments: Experiments):
270
+ def test_repro_restart(
271
+ self, output_path: Path, requested_experiments: dict[str, ExpTestHelper]
272
+ ):
266
273
  """
267
274
  Restart reproducibility test that confirms two short consecutive
268
275
  1-day model runs give the same results as a longer single 2-day model
269
276
  run.
270
277
  """
271
278
  # Get experiments with 2x1 day and 2 day runtimes
272
- experiments.check_experiments([EXP_1D_RUNTIME, EXP_2D_RUNTIME])
273
- exp_1d_runtime = experiments.get_experiment(EXP_1D_RUNTIME)
274
- exp_2d_runtime = experiments.get_experiment(EXP_2D_RUNTIME)
279
+ exp_1d_runtime = requested_experiments.get(EXP_1D_RUNTIME)
280
+ exp_2d_runtime = requested_experiments.get(EXP_2D_RUNTIME)
275
281
 
276
282
  # Now compare the output between our two short and one long run.
277
283
  checksums_1d_0 = exp_1d_runtime.extract_checksums()
@@ -305,14 +311,15 @@ class TestBitReproducibility:
305
311
  EXP_1D_RUNTIME_REPEAT: {"n_runs": 2, "model_runtime": DAY_IN_SECONDS},
306
312
  }
307
313
  )
308
- def test_repro_determinism_restart(self, experiments: Experiments):
314
+ def test_repro_determinism_restart(
315
+ self, requested_experiments: dict[str, ExpTestHelper]
316
+ ):
309
317
  """
310
318
  Determinism test that confirms repeated experiments with two
311
319
  consecutive 1-day model runs give the same results
312
320
  """
313
- experiments.check_experiments([EXP_1D_RUNTIME, EXP_1D_RUNTIME_REPEAT])
314
- exp_1d_runtime = experiments.get_experiment(EXP_1D_RUNTIME)
315
- exp_1d_runtime_repeat = experiments.get_experiment(EXP_1D_RUNTIME_REPEAT)
321
+ exp_1d_runtime = requested_experiments.get(EXP_1D_RUNTIME)
322
+ exp_1d_runtime_repeat = requested_experiments.get(EXP_1D_RUNTIME_REPEAT)
316
323
 
317
324
  # Extract checksums, using the output from the second model run
318
325
  expected = exp_1d_runtime.extract_checksums(exp_1d_runtime.model.output_1)
@@ -136,9 +136,30 @@ class ExpTestHelper:
136
136
  # Change to experiment directory and run.
137
137
  os.chdir(self.control_path)
138
138
 
139
- print("Running payu setup and payu sweep commands")
140
- sp.run(["payu", "setup", "--lab", str(self.lab_path)], check=True)
141
- sp.run(["payu", "sweep", "--lab", str(self.lab_path)], check=True)
139
+ print("Running payu setup")
140
+ result = sp.run(
141
+ ["payu", "setup", "--lab", str(self.lab_path)],
142
+ capture_output=True,
143
+ text=True,
144
+ )
145
+ if result.returncode != 0:
146
+ # Add additional error messaging for debugging
147
+ error_msg = (
148
+ "Failed to run payu setup:\n"
149
+ f"Return code: {result.returncode}\n"
150
+ f"--- stdout ---\n{result.stdout}\n"
151
+ f"--- stderr ---\n{result.stderr}"
152
+ )
153
+ print(error_msg)
154
+ raise RuntimeError(error_msg)
155
+
156
+ print("Running payu sweep")
157
+ sp.run(
158
+ ["payu", "sweep", "--lab", str(self.lab_path)],
159
+ capture_output=True,
160
+ text=True,
161
+ check=True,
162
+ )
142
163
 
143
164
  run_command = ["payu", "run", "--lab", str(self.lab_path)]
144
165
  if n_runs:
@@ -208,7 +229,7 @@ class Experiments:
208
229
  self.output_path = output_path
209
230
  self.keep_archive = keep_archive
210
231
  self.experiments = {}
211
- self.successful_experiments = []
232
+ self.experiment_errors = {}
212
233
 
213
234
  def setup_and_submit(
214
235
  self,
@@ -282,22 +303,27 @@ class Experiments:
282
303
  try:
283
304
  exp.wait_for_payu_run()
284
305
  print(f"Experiment {exp_name} completed successfully")
285
- self.successful_experiments.append(exp_name)
286
306
  except RuntimeError as e:
307
+ self.experiment_errors[exp_name] = str(e)
287
308
  if catch_errors:
288
- print(f"Error in experiment {exp_name}: {e}")
309
+ print(f"Error running experiment {exp_name}: {e}")
289
310
  else:
290
- raise e
311
+ raise
291
312
 
292
- def check_experiments(self, exp_names=list[str]) -> None:
313
+ def check_experiment(self, exp_name: str) -> None:
293
314
  """
294
- Check whether given experiments names have run successfully
315
+ Check whether given experiment name has run successfully
295
316
  """
296
- for exp_name in exp_names:
297
- # TODO: Is there other useful information to display here?
298
- assert (
299
- exp_name in self.successful_experiments
300
- ), f"There was an error running experiment: {exp_name}"
317
+ if exp_name in self.experiment_errors:
318
+ raise RuntimeError(
319
+ f"There was an error running experiment {exp_name}:"
320
+ f" {self.experiment_errors[exp_name]}"
321
+ )
322
+
323
+ # Double check if the required experiment output exists
324
+ exp = self.experiments.get(exp_name)
325
+ if not exp.model.output_exists():
326
+ raise RuntimeError(f"Experiment {exp_name} output file does not exist.")
301
327
 
302
328
 
303
329
  def setup_exp(
@@ -519,13 +545,13 @@ def wait_for_qsub_job(
519
545
  # Check whether the run job was successful
520
546
  exit_status = parse_exit_status_from_file(stdout)
521
547
  if exit_status != 0:
522
- print(
548
+ raise RuntimeError(
549
+ f"Payu {job_type} job failed with exit status {exit_status}:\n"
523
550
  f"Job_ID: {job_id}\n"
524
551
  f"Output files: {output_files}\n"
525
552
  f"--- stdout ---\n{stdout}\n"
526
553
  f"--- stderr ---\n{stderr}\n"
527
554
  )
528
- raise RuntimeError(f"Payu {job_type} job failed with exit status {exit_status}")
529
555
 
530
556
  return stdout, stderr, output_files
531
557
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: model_config_tests
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Test for ACCESS model (payu) configurations
5
5
  Author: ACCESS-NRI
6
6
  License: Apache-2.0
@@ -1,13 +1,14 @@
1
1
  import shutil
2
2
  import subprocess
3
3
  from pathlib import Path
4
- from unittest.mock import patch
4
+ from unittest.mock import Mock, patch
5
5
 
6
6
  import pytest
7
7
  import yaml
8
8
  from netCDF4 import Dataset
9
9
 
10
10
  from model_config_tests.exp_test_helper import (
11
+ Experiments,
11
12
  ExpTestHelper,
12
13
  parse_exit_status_from_file,
13
14
  parse_gadi_pbs_ids,
@@ -127,6 +128,7 @@ def test_experiment_setup_for_test_run_remove_postprocessing(exp, tmp_path):
127
128
  @patch("subprocess.run")
128
129
  def test_experiment_submit_payu_run(mock_run, exp):
129
130
  mock_run.return_value.stdout = "1234567.gadi-pbs\nsome other output"
131
+ mock_run.return_value.returncode = 0
130
132
 
131
133
  current_working_dir = Path.cwd()
132
134
  exp.submit_payu_run()
@@ -150,6 +152,7 @@ def test_experiment_submit_payu_run(mock_run, exp):
150
152
  def test_experiment_submit_payu_run_n_runs(mock_run, exp):
151
153
  """Test --n-runs is added to the payu run command"""
152
154
  mock_run.return_value.stdout = "1234567.gadi-pbs\nsome other output"
155
+ mock_run.return_value.returncode = 0
153
156
 
154
157
  exp.submit_payu_run(n_runs=2)
155
158
 
@@ -172,13 +175,33 @@ def test_experiment_submit_payu_run_disabled(mock_run, exp):
172
175
  assert job_id is None
173
176
 
174
177
 
178
+ @patch("subprocess.run")
179
+ def test_experiment_submit_payu_run_setup_error(mock_run, exp):
180
+ """Test that an error is raised when payu setup fails"""
181
+ mock_run.return_value.stdout = "Some output"
182
+ mock_run.return_value.stderr = "Some error"
183
+ mock_run.return_value.returncode = 1
184
+
185
+ with pytest.raises(RuntimeError, match="Failed to run payu setup*"):
186
+ exp.submit_payu_run()
187
+
188
+ assert exp.run_id is None
189
+
190
+
175
191
  @patch("subprocess.run")
176
192
  def test_experiment_submit_payu_run_error(mock_run, exp):
177
- """Test that an error is raised when any payu command fails"""
178
- mock_run.side_effect = subprocess.CalledProcessError(
179
- returncode=1, cmd="payu setup", output="Some error"
180
- )
181
- mock_run.return_value.stdout = "Some error"
193
+ """Test that an RuntimeError is raised with CalledProcessError"""
194
+ # Mock the first call to payu setup to succeed
195
+ # and subsequent payu command to fail
196
+ setup_success = Mock()
197
+ setup_success.stdout = "Setup successful"
198
+ setup_success.returncode = 0
199
+ mock_run.side_effect = [
200
+ setup_success,
201
+ subprocess.CalledProcessError(
202
+ returncode=1, cmd="payu run", output="Some error"
203
+ ),
204
+ ]
182
205
 
183
206
  with pytest.raises(RuntimeError, match="Failed to submit payu run.*"):
184
207
  exp.submit_payu_run()
@@ -490,3 +513,46 @@ def test_extract_checksums_split_uses_first_tile(exp_with_restarts):
490
513
 
491
514
  checksums = exp_accessom3.extract_checksums(output_directory=exp_accessom3.output_0)
492
515
  assert checksums["output"]["DTBT"][0] == "AC87F8AC28BD1436"
516
+
517
+
518
+ def test_experiments_check_experiment_error(tmp_path):
519
+ with patch("model_config_tests.exp_test_helper.setup_exp") as mock_setup_exp:
520
+ # Create an experiment that will error later on
521
+ mock_error_exp = Mock(autospec=ExpTestHelper)
522
+ mock_setup_exp.return_value = mock_error_exp
523
+ mock_error_exp.wait_for_payu_run.side_effect = RuntimeError(
524
+ "Payu run job failed with exit status 1"
525
+ )
526
+
527
+ exps = Experiments(
528
+ control_path=tmp_path / "control",
529
+ output_path=tmp_path / "output",
530
+ keep_archive=True,
531
+ )
532
+ exps.setup_and_submit(exp_name="error_exp")
533
+ assert exps.experiments["error_exp"] == mock_error_exp
534
+
535
+ # Add a second experiment that will succeed
536
+ mock_success_exp = Mock(autospec=ExpTestHelper)
537
+ mock_success_exp.wait_for_payu_run.return_value = None
538
+ mock_setup_exp.return_value = mock_success_exp
539
+
540
+ exps.setup_and_submit(exp_name="success_exp")
541
+ assert exps.experiments["success_exp"] == mock_success_exp
542
+
543
+ # Check no errors are raised here
544
+ exps.wait_for_all_experiments(catch_errors=True)
545
+ assert exps.experiment_errors == {
546
+ "error_exp": "Payu run job failed with exit status 1"
547
+ }
548
+
549
+ # Check no errors with successful experiment
550
+ exps.check_experiment("success_exp")
551
+
552
+ # Check error raised for the failed experiment
553
+ error_msg = (
554
+ "There was an error running experiment error_exp: "
555
+ "Payu run job failed with exit status 1"
556
+ )
557
+ with pytest.raises(RuntimeError, match=error_msg):
558
+ exps.check_experiment("error_exp")