model-config-tests 0.2.2__tar.gz → 0.2.4__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.
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/PKG-INFO +18 -5
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/README.md +17 -4
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/pyproject.toml +3 -1
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/_version.py +3 -3
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/config_tests/conftest.py +12 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/config_tests/qa/test_access_esm1p6_config.py +19 -4
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/config_tests/qa/test_access_om2_config.py +6 -16
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/config_tests/qa/test_config.py +10 -3
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/config_tests/test_bit_reproducibility.py +62 -26
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/exp_test_helper.py +129 -16
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests.egg-info/PKG-INFO +18 -5
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/tests/test_exp_test_helper.py +162 -6
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/LICENSE +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/setup.cfg +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/setup.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/__init__.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/cmds/compare_exp_tests_cmd.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/cmds/config_tests_cmd.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/compare_exp_tests/conftest.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/compare_exp_tests/test_repro.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/config_tests/qa/test_access_esm1p5_config.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/config_tests/qa/test_access_om3_config.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/__init__.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/accessesm1p5.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/accessesm1p6.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/accessom2.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/accessom3.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/model.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/mom5.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/um7.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/util.py +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests.egg-info/SOURCES.txt +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests.egg-info/dependency_links.txt +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests.egg-info/entry_points.txt +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests.egg-info/requires.txt +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests.egg-info/top_level.txt +0 -0
- {model_config_tests-0.2.2 → model_config_tests-0.2.4}/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.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Test for ACCESS model (payu) configurations
|
|
5
5
|
Author: ACCESS-NRI
|
|
6
6
|
License: Apache-2.0
|
|
@@ -38,11 +38,19 @@ Code from these pytests is adapted from COSIMAS's ACCESS-OM2's [bit reproducibil
|
|
|
38
38
|
|
|
39
39
|
### How to run pytests manually on NCI
|
|
40
40
|
|
|
41
|
-
1. Load payu module - this provides the dependencies needed to run the model
|
|
41
|
+
1. Load payu module - this provides the dependencies needed to run the model.
|
|
42
42
|
|
|
43
43
|
```sh
|
|
44
44
|
module use /g/data/vk83/modules
|
|
45
|
-
module load payu
|
|
45
|
+
module load payu
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Some model configurations may require a minimum payu version, specified in `config.yaml` as `payu_minimum_version`. Please ensure that your loaded payu module meets the requirement.
|
|
49
|
+
If you need to run the model with a development version of payu, please use `payu/dev` instead:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
module use /g/data/vk83/modules
|
|
53
|
+
module load payu/dev
|
|
46
54
|
```
|
|
47
55
|
|
|
48
56
|
2. Create and activate a python virtual environment for installing and running tests
|
|
@@ -52,10 +60,10 @@ Code from these pytests is adapted from COSIMAS's ACCESS-OM2's [bit reproducibil
|
|
|
52
60
|
source <path/to/test-venv>/bin/activate
|
|
53
61
|
```
|
|
54
62
|
|
|
55
|
-
3. Either pip install
|
|
63
|
+
3. Either pip install the latest released version of `model-config-tests`,
|
|
56
64
|
|
|
57
65
|
```sh
|
|
58
|
-
pip install model-config-tests
|
|
66
|
+
pip install model-config-tests
|
|
59
67
|
```
|
|
60
68
|
|
|
61
69
|
Or to install `model-config-tests` in "editable" mode, first clone the repository, and then run pip install from the repository. This means any changes to the code are reflected in the installed package.
|
|
@@ -118,10 +126,15 @@ Running all tests in the pytest suite on a configuration will likely fail as the
|
|
|
118
126
|
- `repro_determinism`: Determinism test that confirms repeated model runs give the same result.
|
|
119
127
|
- `repro_determinism_restart`: Determinism test that confirms repeated experiments with two consecutive runs give the same result.
|
|
120
128
|
- `repro_restart`: Restart reproducibility test that confirms two short consecutive model runs give the same result as a longer single model run.
|
|
129
|
+
- `repro_payu_setup`: Test payu setup reproducibility; fail if MD5 of any file in manifest is changed.
|
|
130
|
+
- `manifests_unchanged`: Uses `git diff` to check manifests are up-to-date. If only fast hashes (e.g. `binhash`) are different, the manifests are reproducible, but `payu setup` may take longer to run as `md5` hashes need to be recalculated. This test is not intended for tagged configurations.
|
|
131
|
+
- `manifests`: A shortcut to run both `manifests_unchanged` and `repro_payu_setup`.
|
|
121
132
|
- `slow`: Tests that are slow to run
|
|
122
133
|
- `dev_config`: General configuration QA tests.
|
|
123
134
|
- `config`: Configuration QA tests for released branches. This includes the `dev_config` tests.
|
|
124
135
|
|
|
136
|
+
|
|
137
|
+
|
|
125
138
|
There are also model-specific markers for configuration QA tests, e.g., `access_om2`, `access_esm1p5`, `access_om3` and `access_esm1p6`. For a list of all available markers,
|
|
126
139
|
run:
|
|
127
140
|
|
|
@@ -10,11 +10,19 @@ Code from these pytests is adapted from COSIMAS's ACCESS-OM2's [bit reproducibil
|
|
|
10
10
|
|
|
11
11
|
### How to run pytests manually on NCI
|
|
12
12
|
|
|
13
|
-
1. Load payu module - this provides the dependencies needed to run the model
|
|
13
|
+
1. Load payu module - this provides the dependencies needed to run the model.
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
16
|
module use /g/data/vk83/modules
|
|
17
|
-
module load payu
|
|
17
|
+
module load payu
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Some model configurations may require a minimum payu version, specified in `config.yaml` as `payu_minimum_version`. Please ensure that your loaded payu module meets the requirement.
|
|
21
|
+
If you need to run the model with a development version of payu, please use `payu/dev` instead:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
module use /g/data/vk83/modules
|
|
25
|
+
module load payu/dev
|
|
18
26
|
```
|
|
19
27
|
|
|
20
28
|
2. Create and activate a python virtual environment for installing and running tests
|
|
@@ -24,10 +32,10 @@ Code from these pytests is adapted from COSIMAS's ACCESS-OM2's [bit reproducibil
|
|
|
24
32
|
source <path/to/test-venv>/bin/activate
|
|
25
33
|
```
|
|
26
34
|
|
|
27
|
-
3. Either pip install
|
|
35
|
+
3. Either pip install the latest released version of `model-config-tests`,
|
|
28
36
|
|
|
29
37
|
```sh
|
|
30
|
-
pip install model-config-tests
|
|
38
|
+
pip install model-config-tests
|
|
31
39
|
```
|
|
32
40
|
|
|
33
41
|
Or to install `model-config-tests` in "editable" mode, first clone the repository, and then run pip install from the repository. This means any changes to the code are reflected in the installed package.
|
|
@@ -90,10 +98,15 @@ Running all tests in the pytest suite on a configuration will likely fail as the
|
|
|
90
98
|
- `repro_determinism`: Determinism test that confirms repeated model runs give the same result.
|
|
91
99
|
- `repro_determinism_restart`: Determinism test that confirms repeated experiments with two consecutive runs give the same result.
|
|
92
100
|
- `repro_restart`: Restart reproducibility test that confirms two short consecutive model runs give the same result as a longer single model run.
|
|
101
|
+
- `repro_payu_setup`: Test payu setup reproducibility; fail if MD5 of any file in manifest is changed.
|
|
102
|
+
- `manifests_unchanged`: Uses `git diff` to check manifests are up-to-date. If only fast hashes (e.g. `binhash`) are different, the manifests are reproducible, but `payu setup` may take longer to run as `md5` hashes need to be recalculated. This test is not intended for tagged configurations.
|
|
103
|
+
- `manifests`: A shortcut to run both `manifests_unchanged` and `repro_payu_setup`.
|
|
93
104
|
- `slow`: Tests that are slow to run
|
|
94
105
|
- `dev_config`: General configuration QA tests.
|
|
95
106
|
- `config`: Configuration QA tests for released branches. This includes the `dev_config` tests.
|
|
96
107
|
|
|
108
|
+
|
|
109
|
+
|
|
97
110
|
There are also model-specific markers for configuration QA tests, e.g., `access_om2`, `access_esm1p5`, `access_om3` and `access_esm1p6`. For a list of all available markers,
|
|
98
111
|
run:
|
|
99
112
|
|
|
@@ -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
|
-
"
|
|
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": "
|
|
11
|
+
"date": "2026-06-02T16:10:30+1000",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "0.2.
|
|
14
|
+
"full-revisionid": "ee4cb743816648753c36e15e6a630bedc2859853",
|
|
15
|
+
"version": "0.2.4"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -131,6 +131,18 @@ def pytest_configure(config):
|
|
|
131
131
|
"markers",
|
|
132
132
|
"repro_determinism_restart: mark tests that check determinism restart",
|
|
133
133
|
)
|
|
134
|
+
config.addinivalue_line(
|
|
135
|
+
"markers",
|
|
136
|
+
"repro_payu_setup: mark tests that check payu setup reproducibility",
|
|
137
|
+
)
|
|
138
|
+
config.addinivalue_line(
|
|
139
|
+
"markers",
|
|
140
|
+
"manifests: mark tests that check payu setup does not change manifests files or md5",
|
|
141
|
+
)
|
|
142
|
+
config.addinivalue_line(
|
|
143
|
+
"markers",
|
|
144
|
+
"manifests_unchanged: mark tests that check payu setup does not change manifests files",
|
|
145
|
+
)
|
|
134
146
|
config.addinivalue_line("markers", "slow: mark tests that are slow to run")
|
|
135
147
|
config.addinivalue_line(
|
|
136
148
|
"markers",
|
|
@@ -27,8 +27,9 @@ ACCESS_ESM1P6_REPOSITORY_NAME = "ACCESS-ESM1.6"
|
|
|
27
27
|
VALID_REALMS: set[str] = {"atmos", "land", "ocean", "ocnBgchem", "seaIce"}
|
|
28
28
|
VALID_KEYWORDS: set[str] = {"global", "access-esm1.6"}
|
|
29
29
|
VALID_NOMINAL_RESOLUTION: str = "100 km"
|
|
30
|
-
# TODO:
|
|
31
|
-
|
|
30
|
+
# TODO: Update this reference when ESM1.6 paper is ready
|
|
31
|
+
VALID_REFERENCE_1p6: str = "https://doi.org/10.5281/zenodo.17490072"
|
|
32
|
+
VALID_URL: str = "https://github.com/ACCESS-NRI/access-esm1.6-configs.git"
|
|
32
33
|
VALID_RUNTIME: dict[str, int] = {"years": 1, "months": 0, "days": 0}
|
|
33
34
|
VALID_RESTART_FREQ: str = "10YS"
|
|
34
35
|
VALID_MPPNCCOMBINE_EXE: str = "mppnccombine.spack"
|
|
@@ -150,8 +151,9 @@ class TestAccessEsm1p6:
|
|
|
150
151
|
"field,expected",
|
|
151
152
|
[
|
|
152
153
|
("nominal_resolution", VALID_NOMINAL_RESOLUTION),
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
("model", ACCESS_ESM1P6_REPOSITORY_NAME),
|
|
155
|
+
("url", VALID_URL),
|
|
156
|
+
("reference", VALID_REFERENCE_1p6),
|
|
155
157
|
],
|
|
156
158
|
)
|
|
157
159
|
def test_metadata_field_equal_expected_value(self, field, expected, metadata):
|
|
@@ -159,6 +161,19 @@ class TestAccessEsm1p6:
|
|
|
159
161
|
field, "metadata.yaml", expected
|
|
160
162
|
)
|
|
161
163
|
|
|
164
|
+
@pytest.mark.parametrize(
|
|
165
|
+
"field",
|
|
166
|
+
[
|
|
167
|
+
("description"),
|
|
168
|
+
("notes"),
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
def test_metadata_not_contain_esm1p5(self, field, metadata):
|
|
172
|
+
"""Check that some fields in metadata do not contain 'ESM1.5', e.g., notes and description."""
|
|
173
|
+
assert (
|
|
174
|
+
field in metadata and "ESM1.5" not in metadata[field]
|
|
175
|
+
), f"Field '{field}' in metadata.yaml should not contain 'ESM1.5'. "
|
|
176
|
+
|
|
162
177
|
def test_config_runtime(self, config):
|
|
163
178
|
assert (
|
|
164
179
|
"calendar" in config
|
|
@@ -66,10 +66,13 @@ class AccessOM2Branch:
|
|
|
66
66
|
self.set_resolution()
|
|
67
67
|
|
|
68
68
|
self.is_high_resolution = self.resolution in ["025deg", "01deg"]
|
|
69
|
-
|
|
69
|
+
is_bgc_old = "bgc" in branch_name
|
|
70
|
+
is_bgc_new = "wombat" in branch_name
|
|
71
|
+
self.is_bgc = is_bgc_old or is_bgc_new
|
|
70
72
|
|
|
71
73
|
# Set expected module and model repository names
|
|
72
|
-
if
|
|
74
|
+
if is_bgc_old:
|
|
75
|
+
# Pre-generic-tracers BGC uses a separate exe
|
|
73
76
|
self.module_name = ACCESS_OM2_BGC_MODULE_NAME
|
|
74
77
|
self.model_repository_name = ACCESS_OM2_BGC_REPOSITORY_NAME
|
|
75
78
|
else:
|
|
@@ -116,7 +119,7 @@ class TestAccessOM2:
|
|
|
116
119
|
|
|
117
120
|
def test_mppncombine_fast_collate_exe(self, config, branch):
|
|
118
121
|
if branch.is_high_resolution:
|
|
119
|
-
pattern = r"
|
|
122
|
+
pattern = r".*mppnccombine-fast"
|
|
120
123
|
if "collate" in config:
|
|
121
124
|
assert re.match(
|
|
122
125
|
pattern, config["collate"]["exe"]
|
|
@@ -126,19 +129,6 @@ class TestAccessOM2:
|
|
|
126
129
|
"mpi"
|
|
127
130
|
], "Expect `mpi: true` when using mppnccombine-fast"
|
|
128
131
|
|
|
129
|
-
def test_sync_userscript_ice_concatenation(self, config):
|
|
130
|
-
# This script runs in the sync pbs job before syncing output to a
|
|
131
|
-
# remote location
|
|
132
|
-
script = "/g/data/vk83/apps/om2-scripts/concatenate_ice/concat_ice_daily.sh"
|
|
133
|
-
assert (
|
|
134
|
-
"userscripts" in config
|
|
135
|
-
and "sync" in config["userscripts"]
|
|
136
|
-
and config["userscripts"]["sync"] == script
|
|
137
|
-
), (
|
|
138
|
-
"Expect sync userscript set to ice-concatenation script."
|
|
139
|
-
+ f"\nuserscript:\n sync: {script}"
|
|
140
|
-
)
|
|
141
|
-
|
|
142
132
|
def test_metadata_realm(self, metadata, branch):
|
|
143
133
|
expected_realms = {"ocean", "seaIce"}
|
|
144
134
|
expected_config = "realm:\n - ocean\n - seaIce"
|
|
@@ -225,11 +225,18 @@ class TestConfig:
|
|
|
225
225
|
"enable"
|
|
226
226
|
], "Sync to remote archive should not be enabled"
|
|
227
227
|
|
|
228
|
-
def
|
|
228
|
+
def test_sync_base_path_is_not_set(self, config):
|
|
229
229
|
if "sync" in config:
|
|
230
230
|
assert not (
|
|
231
|
-
"
|
|
232
|
-
|
|
231
|
+
"base_path" in config["sync"]
|
|
232
|
+
and config["sync"]["base_path"] is not None
|
|
233
|
+
), "Sync base path to remote archive should not be configured"
|
|
234
|
+
|
|
235
|
+
def test_sync_path_not_exists(self, config):
|
|
236
|
+
if "sync" in config:
|
|
237
|
+
assert (
|
|
238
|
+
"path" not in config["sync"]
|
|
239
|
+
), "Sync path should not exist since base_path is preferred"
|
|
233
240
|
|
|
234
241
|
def test_experiment_name_is_not_defined(self, config):
|
|
235
242
|
assert "experiment" not in config, (
|
|
@@ -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, setup_exp
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
244
|
-
|
|
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(
|
|
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
|
-
|
|
273
|
-
|
|
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(
|
|
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
|
-
|
|
314
|
-
|
|
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)
|
|
@@ -321,3 +328,32 @@ class TestBitReproducibility:
|
|
|
321
328
|
)
|
|
322
329
|
|
|
323
330
|
assert produced == expected
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@pytest.mark.repro
|
|
334
|
+
@pytest.mark.manifests
|
|
335
|
+
@pytest.mark.repro_payu_setup
|
|
336
|
+
def test_repro_payu_setup(control_path, output_path):
|
|
337
|
+
"""
|
|
338
|
+
Test payu setup with `--repro` flag which errors if md5 of any files in payu manifests are changed.
|
|
339
|
+
"""
|
|
340
|
+
experiment = setup_exp(control_path, output_path, exp_name="repro_payu_setup")
|
|
341
|
+
try:
|
|
342
|
+
experiment.setup_reproduce()
|
|
343
|
+
except Exception as error:
|
|
344
|
+
pytest.fail(f"{error}")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@pytest.mark.manifests
|
|
348
|
+
@pytest.mark.manifests_unchanged
|
|
349
|
+
def test_manifests_unchanged(control_path, output_path):
|
|
350
|
+
"""
|
|
351
|
+
Test payu setup with `git diff` which errors if any files in payu manifests are changed.
|
|
352
|
+
"""
|
|
353
|
+
experiment = setup_exp(
|
|
354
|
+
control_path, output_path, exp_name="setup_unchanged_manifests"
|
|
355
|
+
)
|
|
356
|
+
try:
|
|
357
|
+
experiment.setup_manifests_unchanged()
|
|
358
|
+
except Exception as error:
|
|
359
|
+
pytest.fail(f"{error}")
|
{model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/exp_test_helper.py
RENAMED
|
@@ -82,6 +82,93 @@ class ExpTestHelper:
|
|
|
82
82
|
"""
|
|
83
83
|
return self.model.output_exists()
|
|
84
84
|
|
|
85
|
+
def setup(self, reproduce=False):
|
|
86
|
+
"""
|
|
87
|
+
Run payu setup command. If reproduce is True, run with --reproduce flag
|
|
88
|
+
to check if md5 hashes have changed in the manifests.
|
|
89
|
+
"""
|
|
90
|
+
owd = Path.cwd()
|
|
91
|
+
# Change to experiment directory and run.
|
|
92
|
+
os.chdir(self.control_path)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
setup_command = [
|
|
96
|
+
"payu",
|
|
97
|
+
"setup",
|
|
98
|
+
"--lab",
|
|
99
|
+
str(self.lab_path),
|
|
100
|
+
]
|
|
101
|
+
if reproduce:
|
|
102
|
+
setup_command.append("--reproduce")
|
|
103
|
+
print(f"Running payu setup command: {setup_command}")
|
|
104
|
+
result = sp.run(setup_command, capture_output=True, text=True)
|
|
105
|
+
|
|
106
|
+
finally:
|
|
107
|
+
# Change back to original working directory
|
|
108
|
+
os.chdir(owd)
|
|
109
|
+
|
|
110
|
+
if result.returncode != 0:
|
|
111
|
+
raise RuntimeError(
|
|
112
|
+
"Failed to run payu setup"
|
|
113
|
+
+ (" with --reproduce.\n" if reproduce else ".\n")
|
|
114
|
+
+ f"{'='*10}STDOUT{'='*10}\n {result.stdout}\n"
|
|
115
|
+
f"{'='*10}STDERR{'='*10}\n {result.stderr}\n"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def run_git_diff(self, path, extra_args=None):
|
|
119
|
+
"""
|
|
120
|
+
Run git diff command on the given path and return the output.
|
|
121
|
+
"""
|
|
122
|
+
command = ["git", "-C", str(path), "diff"] + extra_args if extra_args else []
|
|
123
|
+
|
|
124
|
+
result = sp.run(command, capture_output=True, text=True)
|
|
125
|
+
|
|
126
|
+
if result.returncode != 0:
|
|
127
|
+
raise RuntimeError(
|
|
128
|
+
f"Git command failed with exit code {result.returncode}.\n"
|
|
129
|
+
f"{'='*10}STDOUT{'='*10}\n {result.stdout}\n"
|
|
130
|
+
f"{'='*10}STDERR{'='*10}\n {result.stderr}\n"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return result.stdout
|
|
134
|
+
|
|
135
|
+
def setup_reproduce(self):
|
|
136
|
+
"""
|
|
137
|
+
Run payu setup with `--repro` flag to check if md5 hashes have changed in the manifests.
|
|
138
|
+
"""
|
|
139
|
+
self.setup(reproduce=True)
|
|
140
|
+
|
|
141
|
+
def setup_manifests_unchanged(self):
|
|
142
|
+
"""
|
|
143
|
+
Run payu setup command and check if manifests files have been changed with `git diff`.
|
|
144
|
+
"""
|
|
145
|
+
self.setup(reproduce=False)
|
|
146
|
+
|
|
147
|
+
result = self.run_git_diff(
|
|
148
|
+
self.control_path, extra_args=["--name-only", "manifests/"]
|
|
149
|
+
)
|
|
150
|
+
if result != "":
|
|
151
|
+
# Collect and display the top 10 lines of the diff for each modified file
|
|
152
|
+
files = result.strip().split("\n")
|
|
153
|
+
error_message = "Modifications are detected in file:\n"
|
|
154
|
+
error_message += "\n".join(" - " + file for file in files) + "\n"
|
|
155
|
+
error_message += "\nIf md5 hashes have changed, this indicates file contents being different."
|
|
156
|
+
error_message += """
|
|
157
|
+
If binhashes/paths have changed but md5's are the same,
|
|
158
|
+
this will mean the configuration can reproduce the manifests
|
|
159
|
+
but `payu setup` will take longer to run as it needs to re-calculate all the md5 hashes.
|
|
160
|
+
"""
|
|
161
|
+
for file in files:
|
|
162
|
+
diff_details = self.run_git_diff(
|
|
163
|
+
self.control_path, extra_args=[f"{file}"]
|
|
164
|
+
)
|
|
165
|
+
diff_lines = diff_details.splitlines()
|
|
166
|
+
top_lines = "\n".join(diff_lines[2:12])
|
|
167
|
+
if len(diff_lines) > 12:
|
|
168
|
+
top_lines += "\n... (truncated)"
|
|
169
|
+
error_message += f"\n{'='*10} Diff for {file} {'='*10}\n{top_lines}\n"
|
|
170
|
+
raise RuntimeError(f"{error_message}")
|
|
171
|
+
|
|
85
172
|
def setup_for_test_run(self):
|
|
86
173
|
"""
|
|
87
174
|
Various config.yaml settings need to be modified in order to run in the
|
|
@@ -136,9 +223,30 @@ class ExpTestHelper:
|
|
|
136
223
|
# Change to experiment directory and run.
|
|
137
224
|
os.chdir(self.control_path)
|
|
138
225
|
|
|
139
|
-
print("Running payu setup
|
|
140
|
-
sp.run(
|
|
141
|
-
|
|
226
|
+
print("Running payu setup")
|
|
227
|
+
result = sp.run(
|
|
228
|
+
["payu", "setup", "--lab", str(self.lab_path)],
|
|
229
|
+
capture_output=True,
|
|
230
|
+
text=True,
|
|
231
|
+
)
|
|
232
|
+
if result.returncode != 0:
|
|
233
|
+
# Add additional error messaging for debugging
|
|
234
|
+
error_msg = (
|
|
235
|
+
"Failed to run payu setup:\n"
|
|
236
|
+
f"Return code: {result.returncode}\n"
|
|
237
|
+
f"--- stdout ---\n{result.stdout}\n"
|
|
238
|
+
f"--- stderr ---\n{result.stderr}"
|
|
239
|
+
)
|
|
240
|
+
print(error_msg)
|
|
241
|
+
raise RuntimeError(error_msg)
|
|
242
|
+
|
|
243
|
+
print("Running payu sweep")
|
|
244
|
+
sp.run(
|
|
245
|
+
["payu", "sweep", "--lab", str(self.lab_path)],
|
|
246
|
+
capture_output=True,
|
|
247
|
+
text=True,
|
|
248
|
+
check=True,
|
|
249
|
+
)
|
|
142
250
|
|
|
143
251
|
run_command = ["payu", "run", "--lab", str(self.lab_path)]
|
|
144
252
|
if n_runs:
|
|
@@ -208,7 +316,7 @@ class Experiments:
|
|
|
208
316
|
self.output_path = output_path
|
|
209
317
|
self.keep_archive = keep_archive
|
|
210
318
|
self.experiments = {}
|
|
211
|
-
self.
|
|
319
|
+
self.experiment_errors = {}
|
|
212
320
|
|
|
213
321
|
def setup_and_submit(
|
|
214
322
|
self,
|
|
@@ -282,22 +390,27 @@ class Experiments:
|
|
|
282
390
|
try:
|
|
283
391
|
exp.wait_for_payu_run()
|
|
284
392
|
print(f"Experiment {exp_name} completed successfully")
|
|
285
|
-
self.successful_experiments.append(exp_name)
|
|
286
393
|
except RuntimeError as e:
|
|
394
|
+
self.experiment_errors[exp_name] = str(e)
|
|
287
395
|
if catch_errors:
|
|
288
|
-
print(f"Error
|
|
396
|
+
print(f"Error running experiment {exp_name}: {e}")
|
|
289
397
|
else:
|
|
290
|
-
raise
|
|
398
|
+
raise
|
|
291
399
|
|
|
292
|
-
def
|
|
400
|
+
def check_experiment(self, exp_name: str) -> None:
|
|
293
401
|
"""
|
|
294
|
-
Check whether given
|
|
402
|
+
Check whether given experiment name has run successfully
|
|
295
403
|
"""
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
)
|
|
404
|
+
if exp_name in self.experiment_errors:
|
|
405
|
+
raise RuntimeError(
|
|
406
|
+
f"There was an error running experiment {exp_name}:"
|
|
407
|
+
f" {self.experiment_errors[exp_name]}"
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Double check if the required experiment output exists
|
|
411
|
+
exp = self.experiments.get(exp_name)
|
|
412
|
+
if not exp.model.output_exists():
|
|
413
|
+
raise RuntimeError(f"Experiment {exp_name} output file does not exist.")
|
|
301
414
|
|
|
302
415
|
|
|
303
416
|
def setup_exp(
|
|
@@ -519,13 +632,13 @@ def wait_for_qsub_job(
|
|
|
519
632
|
# Check whether the run job was successful
|
|
520
633
|
exit_status = parse_exit_status_from_file(stdout)
|
|
521
634
|
if exit_status != 0:
|
|
522
|
-
|
|
635
|
+
raise RuntimeError(
|
|
636
|
+
f"Payu {job_type} job failed with exit status {exit_status}:\n"
|
|
523
637
|
f"Job_ID: {job_id}\n"
|
|
524
638
|
f"Output files: {output_files}\n"
|
|
525
639
|
f"--- stdout ---\n{stdout}\n"
|
|
526
640
|
f"--- stderr ---\n{stderr}\n"
|
|
527
641
|
)
|
|
528
|
-
raise RuntimeError(f"Payu {job_type} job failed with exit status {exit_status}")
|
|
529
642
|
|
|
530
643
|
return stdout, stderr, output_files
|
|
531
644
|
|
{model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: model_config_tests
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Test for ACCESS model (payu) configurations
|
|
5
5
|
Author: ACCESS-NRI
|
|
6
6
|
License: Apache-2.0
|
|
@@ -38,11 +38,19 @@ Code from these pytests is adapted from COSIMAS's ACCESS-OM2's [bit reproducibil
|
|
|
38
38
|
|
|
39
39
|
### How to run pytests manually on NCI
|
|
40
40
|
|
|
41
|
-
1. Load payu module - this provides the dependencies needed to run the model
|
|
41
|
+
1. Load payu module - this provides the dependencies needed to run the model.
|
|
42
42
|
|
|
43
43
|
```sh
|
|
44
44
|
module use /g/data/vk83/modules
|
|
45
|
-
module load payu
|
|
45
|
+
module load payu
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Some model configurations may require a minimum payu version, specified in `config.yaml` as `payu_minimum_version`. Please ensure that your loaded payu module meets the requirement.
|
|
49
|
+
If you need to run the model with a development version of payu, please use `payu/dev` instead:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
module use /g/data/vk83/modules
|
|
53
|
+
module load payu/dev
|
|
46
54
|
```
|
|
47
55
|
|
|
48
56
|
2. Create and activate a python virtual environment for installing and running tests
|
|
@@ -52,10 +60,10 @@ Code from these pytests is adapted from COSIMAS's ACCESS-OM2's [bit reproducibil
|
|
|
52
60
|
source <path/to/test-venv>/bin/activate
|
|
53
61
|
```
|
|
54
62
|
|
|
55
|
-
3. Either pip install
|
|
63
|
+
3. Either pip install the latest released version of `model-config-tests`,
|
|
56
64
|
|
|
57
65
|
```sh
|
|
58
|
-
pip install model-config-tests
|
|
66
|
+
pip install model-config-tests
|
|
59
67
|
```
|
|
60
68
|
|
|
61
69
|
Or to install `model-config-tests` in "editable" mode, first clone the repository, and then run pip install from the repository. This means any changes to the code are reflected in the installed package.
|
|
@@ -118,10 +126,15 @@ Running all tests in the pytest suite on a configuration will likely fail as the
|
|
|
118
126
|
- `repro_determinism`: Determinism test that confirms repeated model runs give the same result.
|
|
119
127
|
- `repro_determinism_restart`: Determinism test that confirms repeated experiments with two consecutive runs give the same result.
|
|
120
128
|
- `repro_restart`: Restart reproducibility test that confirms two short consecutive model runs give the same result as a longer single model run.
|
|
129
|
+
- `repro_payu_setup`: Test payu setup reproducibility; fail if MD5 of any file in manifest is changed.
|
|
130
|
+
- `manifests_unchanged`: Uses `git diff` to check manifests are up-to-date. If only fast hashes (e.g. `binhash`) are different, the manifests are reproducible, but `payu setup` may take longer to run as `md5` hashes need to be recalculated. This test is not intended for tagged configurations.
|
|
131
|
+
- `manifests`: A shortcut to run both `manifests_unchanged` and `repro_payu_setup`.
|
|
121
132
|
- `slow`: Tests that are slow to run
|
|
122
133
|
- `dev_config`: General configuration QA tests.
|
|
123
134
|
- `config`: Configuration QA tests for released branches. This includes the `dev_config` tests.
|
|
124
135
|
|
|
136
|
+
|
|
137
|
+
|
|
125
138
|
There are also model-specific markers for configuration QA tests, e.g., `access_om2`, `access_esm1p5`, `access_om3` and `access_esm1p6`. For a list of all available markers,
|
|
126
139
|
run:
|
|
127
140
|
|
|
@@ -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 MagicMock, 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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
)
|
|
181
|
-
|
|
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,136 @@ 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")
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@patch("subprocess.run")
|
|
562
|
+
def test_setup_reproduce_error(mock_run, exp):
|
|
563
|
+
"""Test that payu setup --repro fails raises an error and return to original work directory"""
|
|
564
|
+
# Mock the payu setup --repro to fail
|
|
565
|
+
mock_result = MagicMock()
|
|
566
|
+
mock_result.returncode = 1
|
|
567
|
+
mock_result.stderr = "MD5 mismatch"
|
|
568
|
+
mock_result.stdout = "Check manifest"
|
|
569
|
+
mock_run.return_value = mock_result
|
|
570
|
+
|
|
571
|
+
# Store original current working directory
|
|
572
|
+
owd = Path.cwd()
|
|
573
|
+
|
|
574
|
+
with pytest.raises(RuntimeError) as excinfo:
|
|
575
|
+
exp.setup_reproduce()
|
|
576
|
+
|
|
577
|
+
assert "Failed to run payu setup with --reproduce.\n" in str(excinfo.value)
|
|
578
|
+
assert f"{'='*10}STDOUT{'='*10}\n {mock_result.stdout}\n" in str(excinfo.value)
|
|
579
|
+
|
|
580
|
+
# assert returning to the original work directory
|
|
581
|
+
assert Path.cwd() == owd
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@patch("subprocess.run")
|
|
585
|
+
def test_setup_manifests_unchanged_fail_setup(mock_run, exp):
|
|
586
|
+
"""Test that an error is raised when payu setup fails in setup_manifests_unchanged()"""
|
|
587
|
+
# Mock the payu setup --repro to fail with unchanged manifests
|
|
588
|
+
mock_result = MagicMock()
|
|
589
|
+
mock_result.returncode = 1
|
|
590
|
+
mock_result.stderr = "Setup failed"
|
|
591
|
+
mock_result.stdout = "Payu setup output"
|
|
592
|
+
mock_run.return_value = mock_result
|
|
593
|
+
|
|
594
|
+
# Store original current working directory
|
|
595
|
+
owd = Path.cwd()
|
|
596
|
+
|
|
597
|
+
with pytest.raises(RuntimeError) as excinfo:
|
|
598
|
+
exp.setup_manifests_unchanged()
|
|
599
|
+
|
|
600
|
+
assert "Failed to run payu setup" in str(excinfo.value)
|
|
601
|
+
assert f"{'='*10}STDOUT{'='*10}\n {mock_result.stdout}\n" in str(excinfo.value)
|
|
602
|
+
|
|
603
|
+
# assert returning to the original work directory
|
|
604
|
+
assert Path.cwd() == owd
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
@patch("subprocess.run")
|
|
608
|
+
def test_setup_manifests_unchanged_show_changes(mock_run, exp):
|
|
609
|
+
"""Test that when manifests are changed, the `git diff` results are printed to stdout"""
|
|
610
|
+
# Mock the `payu setup` succeed first
|
|
611
|
+
setup_success = MagicMock(returncode=0, stdout="Payu setup succeeded")
|
|
612
|
+
|
|
613
|
+
top_lines = """--- a/{diff_file}
|
|
614
|
+
+++ b/{diff_file}
|
|
615
|
+
+new line
|
|
616
|
+
-old line
|
|
617
|
+
"""
|
|
618
|
+
diff_file = "manifests/input.yaml"
|
|
619
|
+
# Then mock the `git diff --name-only` to show which files are changed
|
|
620
|
+
git_diff_name_only = MagicMock(returncode=0, stdout=diff_file)
|
|
621
|
+
|
|
622
|
+
# Mock the `git diff` to show the detailed changes in the file
|
|
623
|
+
git_diff_run = MagicMock(
|
|
624
|
+
returncode=0,
|
|
625
|
+
stdout=(
|
|
626
|
+
f"""diff --git a/{diff_file} b/{diff_file}
|
|
627
|
+
index abc123...zyx789 100111
|
|
628
|
+
"""
|
|
629
|
+
)
|
|
630
|
+
+ top_lines,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Run these mocks in sequence
|
|
634
|
+
mock_run.side_effect = [setup_success, git_diff_name_only, git_diff_run]
|
|
635
|
+
|
|
636
|
+
# Store original current working directory
|
|
637
|
+
owd = Path.cwd()
|
|
638
|
+
|
|
639
|
+
with pytest.raises(RuntimeError) as excinfo:
|
|
640
|
+
exp.setup_manifests_unchanged()
|
|
641
|
+
|
|
642
|
+
assert "Modifications are detected in file:\n" in str(excinfo.value)
|
|
643
|
+
assert f"\n{'='*10} Diff for {diff_file} {'='*10}\n{top_lines}\n" in str(
|
|
644
|
+
excinfo.value
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
# assert returning to the original work directory
|
|
648
|
+
assert Path.cwd() == owd
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/__init__.py
RENAMED
|
File without changes
|
{model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/accessesm1p5.py
RENAMED
|
File without changes
|
{model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/accessesm1p6.py
RENAMED
|
File without changes
|
{model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/accessom2.py
RENAMED
|
File without changes
|
{model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/accessom3.py
RENAMED
|
File without changes
|
{model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests/models/model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests.egg-info/requires.txt
RENAMED
|
File without changes
|
{model_config_tests-0.2.2 → model_config_tests-0.2.4}/src/model_config_tests.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|