climate-ref-core 0.14.2__tar.gz → 0.14.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.
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/PKG-INFO +2 -2
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/pyproject.toml +2 -2
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/dataset_registry.py +25 -25
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/testing.py +84 -1
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_dataset_registry/test_dataset_registry.py +1 -5
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_testing.py +119 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/.gitignore +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/LICENCE +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/NOTICE +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/README.md +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/__init__.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/cmip6_to_cmip7.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/constraints.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/data/cmip6_cmip7_variable_map.json +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/datasets.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/diagnostics.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/env.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/esgf/__init__.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/esgf/base.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/esgf/cmip6.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/esgf/cmip7.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/esgf/fetcher.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/esgf/obs4mips.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/esgf/registry.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/exceptions.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/executor.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/logging.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/metric_values/__init__.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/metric_values/typing.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/providers.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/py.typed +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/pycmec/README.md +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/pycmec/__init__.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/pycmec/controlled_vocabulary.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/pycmec/cv_cmip7_aft.yaml +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/pycmec/metric.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/pycmec/output.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/source_types.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/summary.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/esgf/test_base.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/esgf/test_cmip6.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/esgf/test_cmip7.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/esgf/test_fetcher.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/esgf/test_obs4mips.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/esgf/test_registry.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/metric_values/test_typing.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/pycmec/cmec_testdata/cmec_metric_sample.json +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/pycmec/cmec_testdata/cmec_output_sample.json +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/pycmec/cmec_testdata/cv_sample.yaml +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/pycmec/cmec_testdata/test_metric_json_schema.yml +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/pycmec/cmec_testdata/test_output_json_schema.yml +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/pycmec/conftest.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/pycmec/test_cmec_metric.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/pycmec/test_cmec_output.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/pycmec/test_controlled_vocabulary.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_cmip6_to_cmip7.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_constraints.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_datasets/dataset_collection_hash.yml +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_datasets/dataset_collection_obs4mips_hash.yml +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_datasets/execution_dataset_hash.yml +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_datasets.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_diagnostics.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_exceptions.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_executor.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_logging.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_packaging.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_providers.py +0 -0
- {climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/test_summary.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: climate-ref-core
|
|
3
|
-
Version: 0.14.
|
|
3
|
+
Version: 0.14.4
|
|
4
4
|
Summary: Core library for the CMIP Rapid Evaluation Framework
|
|
5
5
|
Author-email: Jared Lewis <jared.lewis@climate-resource.com>, Mika Pflueger <mika.pflueger@climate-resource.com>, Bouwe Andela <b.andela@esciencecenter.nl>, Jiwoo Lee <lee1043@llnl.gov>, Min Xu <xum1@ornl.gov>, Nathan Collier <collierno@ornl.gov>, Dora Hegedus <dora.hegedus@stfc.ac.uk>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -25,7 +25,7 @@ Requires-Dist: fastprogress==1.0.5
|
|
|
25
25
|
Requires-Dist: intake-esgf>=2025.7.16
|
|
26
26
|
Requires-Dist: loguru>=0.7.0
|
|
27
27
|
Requires-Dist: numpy>=2.0.0
|
|
28
|
-
Requires-Dist: pandas
|
|
28
|
+
Requires-Dist: pandas>=2.1.0
|
|
29
29
|
Requires-Dist: pooch<2,>=1.8.0
|
|
30
30
|
Requires-Dist: pyarrow>=17.0.0
|
|
31
31
|
Requires-Dist: pydantic>=2.10.6
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "climate-ref-core"
|
|
3
|
-
version = "0.14.
|
|
3
|
+
version = "0.14.4"
|
|
4
4
|
description = "Core library for the CMIP Rapid Evaluation Framework"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -48,7 +48,7 @@ dependencies = [
|
|
|
48
48
|
# SPEC 0000 constraints
|
|
49
49
|
# We follow [SPEC-0000](https://scientific-python.org/specs/spec-0000/)
|
|
50
50
|
# which defines a 2-year support window for key libraries and 3-year window for Python versions
|
|
51
|
-
"pandas>=2.1.0
|
|
51
|
+
"pandas>=2.1.0",
|
|
52
52
|
"numpy>=2.0.0",
|
|
53
53
|
|
|
54
54
|
# Temporarily pin fastprogress dependency (from intake-esm) due to bug in recent version
|
{climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/dataset_registry.py
RENAMED
|
@@ -21,6 +21,30 @@ from climate_ref_core.env import env
|
|
|
21
21
|
DATASET_URL = env.str("REF_DATASET_URL", default="https://obs4ref.climate-ref.org")
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def resolve_cache_dir(cache_name: str) -> pathlib.Path:
|
|
25
|
+
"""
|
|
26
|
+
Resolve the cache directory for a registry.
|
|
27
|
+
|
|
28
|
+
If the ``REF_DATASET_CACHE_DIR`` environment variable is set, use that as the root.
|
|
29
|
+
Otherwise, fall back to the OS cache under ``climate_ref``.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
cache_name
|
|
34
|
+
Subdirectory name within the cache root.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
The resolved cache directory path.
|
|
39
|
+
"""
|
|
40
|
+
if env_cache_dir := os.environ.get("REF_DATASET_CACHE_DIR"):
|
|
41
|
+
cache_dir = pathlib.Path(os.path.expandvars(env_cache_dir)).expanduser()
|
|
42
|
+
else:
|
|
43
|
+
cache_dir = pooch.os_cache("climate_ref")
|
|
44
|
+
|
|
45
|
+
return cache_dir / cache_name
|
|
46
|
+
|
|
47
|
+
|
|
24
48
|
def _verify_hash_matches(fname: str | pathlib.Path, known_hash: str) -> bool:
|
|
25
49
|
"""
|
|
26
50
|
Check if the hash of a file matches a known hash.
|
|
@@ -207,30 +231,6 @@ class DatasetRegistryManager:
|
|
|
207
231
|
return list(self._registries.keys())
|
|
208
232
|
|
|
209
233
|
@staticmethod
|
|
210
|
-
def _resolve_cache_dir(cache_name: str) -> pathlib.Path:
|
|
211
|
-
"""
|
|
212
|
-
Resolve the cache directory for a registry.
|
|
213
|
-
|
|
214
|
-
If the ``REF_DATASET_CACHE_DIR`` environment variable is set,
|
|
215
|
-
use that as the root. Otherwise, fall back to the OS cache
|
|
216
|
-
under ``climate_ref``.
|
|
217
|
-
|
|
218
|
-
Parameters
|
|
219
|
-
----------
|
|
220
|
-
cache_name
|
|
221
|
-
Subdirectory name within the cache root.
|
|
222
|
-
|
|
223
|
-
Returns
|
|
224
|
-
-------
|
|
225
|
-
The resolved cache directory path.
|
|
226
|
-
"""
|
|
227
|
-
if env_cache_dir := os.environ.get("REF_DATASET_CACHE_DIR"):
|
|
228
|
-
cache_dir = pathlib.Path(os.path.expandvars(env_cache_dir)).expanduser()
|
|
229
|
-
else:
|
|
230
|
-
cache_dir = pooch.os_cache("climate_ref")
|
|
231
|
-
|
|
232
|
-
return cache_dir / cache_name
|
|
233
|
-
|
|
234
234
|
@staticmethod
|
|
235
235
|
def _migrate_cache(
|
|
236
236
|
registry: pooch.Pooch,
|
|
@@ -314,7 +314,7 @@ class DatasetRegistryManager:
|
|
|
314
314
|
if cache_name is None:
|
|
315
315
|
cache_name = name
|
|
316
316
|
|
|
317
|
-
cache_path =
|
|
317
|
+
cache_path = resolve_cache_dir(cache_name)
|
|
318
318
|
|
|
319
319
|
# Before v0.13.0 everything was cached directly under
|
|
320
320
|
# pooch.os_cache("climate_ref") with no per-registry subdirectory.
|
|
@@ -10,6 +10,7 @@ This module provides:
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import json
|
|
13
14
|
import shutil
|
|
14
15
|
import sys
|
|
15
16
|
from pathlib import Path
|
|
@@ -28,6 +29,7 @@ from climate_ref_core.datasets import (
|
|
|
28
29
|
)
|
|
29
30
|
from climate_ref_core.diagnostics import ExecutionDefinition, ExecutionResult
|
|
30
31
|
from climate_ref_core.esgf.base import ESGFRequest
|
|
32
|
+
from climate_ref_core.metric_values.typing import SeriesMetricValue
|
|
31
33
|
from climate_ref_core.pycmec.metric import CMECMetric
|
|
32
34
|
from climate_ref_core.pycmec.output import CMECOutput
|
|
33
35
|
|
|
@@ -499,6 +501,13 @@ def validate_cmec_bundles(diagnostic: Diagnostic, result: ExecutionResult) -> No
|
|
|
499
501
|
AssertionError
|
|
500
502
|
If the result is not successful or bundles are invalid
|
|
501
503
|
"""
|
|
504
|
+
# TODO: Add content regression checks for the CMEC bundles (diagnostic.json /
|
|
505
|
+
# output.json), mirroring `validate_series_regression`. These bundles are only
|
|
506
|
+
# structurally validated here, so a diagnostic can change the metric/output
|
|
507
|
+
# values without any regression test failing (see issue #703 for the series
|
|
508
|
+
# equivalent). A content comparison must sanitise the regenerated bundle first
|
|
509
|
+
# via `ExecutionRegression.output_replacements`, since the committed bundles
|
|
510
|
+
# store `<OUTPUT_DIR>` / `<TEST_DATA_DIR>` placeholders.
|
|
502
511
|
assert result.successful, f"Execution failed: {result}"
|
|
503
512
|
|
|
504
513
|
# Validate metric bundle
|
|
@@ -517,6 +526,71 @@ def validate_cmec_bundles(diagnostic: Diagnostic, result: ExecutionResult) -> No
|
|
|
517
526
|
CMECOutput.load_from_json(result.to_output_path(result.output_bundle_filename))
|
|
518
527
|
|
|
519
528
|
|
|
529
|
+
def _load_series_sanitised(path: Path, replacements: dict[str, str]) -> list[SeriesMetricValue]:
|
|
530
|
+
"""
|
|
531
|
+
Load series from ``path``, replacing real paths with regression placeholders.
|
|
532
|
+
"""
|
|
533
|
+
text = path.read_text(encoding="utf-8")
|
|
534
|
+
# Apply replacements longest-key-first in case of nested repleacements
|
|
535
|
+
for real, placeholder in sorted(replacements.items(), key=lambda kv: len(kv[0]), reverse=True):
|
|
536
|
+
text = text.replace(real, placeholder)
|
|
537
|
+
data = json.loads(text)
|
|
538
|
+
if not isinstance(data, list):
|
|
539
|
+
raise ValueError(f"Expected a list of series values, got {type(data)}")
|
|
540
|
+
return [SeriesMetricValue.model_validate(s, strict=True) for s in data]
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def validate_series_regression(
|
|
544
|
+
expected_path: Path,
|
|
545
|
+
actual_path: Path,
|
|
546
|
+
*,
|
|
547
|
+
slug: str,
|
|
548
|
+
replacements: dict[str, str] | None = None,
|
|
549
|
+
) -> None:
|
|
550
|
+
"""
|
|
551
|
+
Assert that regenerated series match the committed regression series.
|
|
552
|
+
|
|
553
|
+
If ``expected_path`` does not exist (legacy regression data without a stored series),
|
|
554
|
+
the check is skipped.
|
|
555
|
+
|
|
556
|
+
Parameters
|
|
557
|
+
----------
|
|
558
|
+
expected_path
|
|
559
|
+
Path to the committed ``series.json`` regression artifact. This file
|
|
560
|
+
stores sanitized placeholders (e.g. ``<OUTPUT_DIR>``).
|
|
561
|
+
actual_path
|
|
562
|
+
Path to the freshly regenerated ``series.json`` in the output directory.
|
|
563
|
+
This file contains expanded absolute paths.
|
|
564
|
+
slug
|
|
565
|
+
The diagnostic slug, used for error messages.
|
|
566
|
+
replacements
|
|
567
|
+
Optional ``real path -> placeholder`` mapping applied to the regenerated series before comparison,
|
|
568
|
+
mirroring the sanitisation used when the regression data was written.
|
|
569
|
+
|
|
570
|
+
Raises
|
|
571
|
+
------
|
|
572
|
+
AssertionError
|
|
573
|
+
If the regenerated series differ from the committed series.
|
|
574
|
+
"""
|
|
575
|
+
if not expected_path.exists():
|
|
576
|
+
return
|
|
577
|
+
|
|
578
|
+
expected = SeriesMetricValue.load_from_json(expected_path)
|
|
579
|
+
actual = _load_series_sanitised(actual_path, replacements or {})
|
|
580
|
+
|
|
581
|
+
def _sorted_dump(series: list[SeriesMetricValue]) -> list[dict[str, Any]]:
|
|
582
|
+
return sorted((s.model_dump(mode="json") for s in series), key=lambda s: repr(s["dimensions"]))
|
|
583
|
+
|
|
584
|
+
assert len(actual) == len(expected), (
|
|
585
|
+
f"Diagnostic {slug} produced {len(actual)} series but the committed series.json "
|
|
586
|
+
f"has {len(expected)}. Regenerate the regression data with `--force-regen`."
|
|
587
|
+
)
|
|
588
|
+
assert _sorted_dump(actual) == _sorted_dump(expected), (
|
|
589
|
+
f"Diagnostic {slug} produced series that differ from the committed series.json. "
|
|
590
|
+
f"Regenerate the regression data with `--force-regen`."
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
|
|
520
594
|
@frozen
|
|
521
595
|
class RegressionValidator:
|
|
522
596
|
"""
|
|
@@ -585,10 +659,19 @@ class RegressionValidator:
|
|
|
585
659
|
)
|
|
586
660
|
|
|
587
661
|
def validate(self, definition: ExecutionDefinition) -> None:
|
|
588
|
-
"""Validate CMEC bundles in the regression output."""
|
|
662
|
+
"""Validate CMEC bundles and series in the regression output."""
|
|
589
663
|
result = self.diagnostic.build_execution_result(definition)
|
|
590
664
|
result.to_output_path("out.log").touch() # Log file not tracked in regression
|
|
591
665
|
validate_cmec_bundles(self.diagnostic, result)
|
|
666
|
+
validate_series_regression(
|
|
667
|
+
expected_path=self.paths.regression / "series.json",
|
|
668
|
+
actual_path=definition.output_directory / "series.json",
|
|
669
|
+
slug=self.diagnostic.slug,
|
|
670
|
+
replacements={
|
|
671
|
+
str(definition.output_directory): "<OUTPUT_DIR>",
|
|
672
|
+
str(self.test_data_dir): "<TEST_DATA_DIR>",
|
|
673
|
+
},
|
|
674
|
+
)
|
|
592
675
|
|
|
593
676
|
|
|
594
677
|
def collect_test_case_params(provider: DiagnosticProvider) -> list[ParameterSet]:
|
|
@@ -252,11 +252,7 @@ class TestMigrateCache:
|
|
|
252
252
|
|
|
253
253
|
new_dir = tmp_path / "new_cache" / "test_registry"
|
|
254
254
|
|
|
255
|
-
mocker.patch.
|
|
256
|
-
DatasetRegistryManager,
|
|
257
|
-
"_resolve_cache_dir",
|
|
258
|
-
return_value=new_dir,
|
|
259
|
-
)
|
|
255
|
+
mocker.patch("climate_ref_core.dataset_registry.resolve_cache_dir", return_value=new_dir)
|
|
260
256
|
|
|
261
257
|
registry_path, package, resource = fake_registry_file
|
|
262
258
|
with registry_path.open("w") as f:
|
|
@@ -8,6 +8,7 @@ import yaml
|
|
|
8
8
|
|
|
9
9
|
from climate_ref_core.datasets import DatasetCollection, ExecutionDatasetCollection, SourceDatasetType
|
|
10
10
|
from climate_ref_core.diagnostics import ExecutionResult
|
|
11
|
+
from climate_ref_core.metric_values.typing import SeriesMetricValue
|
|
11
12
|
from climate_ref_core.testing import (
|
|
12
13
|
RegressionValidator,
|
|
13
14
|
TestCase,
|
|
@@ -19,6 +20,7 @@ from climate_ref_core.testing import (
|
|
|
19
20
|
load_datasets_from_yaml,
|
|
20
21
|
save_datasets_to_yaml,
|
|
21
22
|
validate_cmec_bundles,
|
|
23
|
+
validate_series_regression,
|
|
22
24
|
)
|
|
23
25
|
|
|
24
26
|
|
|
@@ -912,3 +914,120 @@ class TestRegressionValidator:
|
|
|
912
914
|
|
|
913
915
|
# Verify build_execution_result was called
|
|
914
916
|
mock_diagnostic.build_execution_result.assert_called_once_with(mock_definition)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def _series(value: float, *, dim: str = "M1", index_name: str = "year", attrs=None) -> SeriesMetricValue:
|
|
920
|
+
"""Build a small SeriesMetricValue for testing."""
|
|
921
|
+
return SeriesMetricValue(
|
|
922
|
+
dimensions={"source_id": dim},
|
|
923
|
+
values=[value, value + 1.0],
|
|
924
|
+
index=["2000", "2001"],
|
|
925
|
+
index_name=index_name,
|
|
926
|
+
attributes=attrs,
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def _write_series(path: Path, series: list[SeriesMetricValue]) -> Path:
|
|
931
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
932
|
+
SeriesMetricValue.dump_to_json(path, series)
|
|
933
|
+
return path
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
class TestValidateSeriesRegression:
|
|
937
|
+
def test_matching_series_passes(self, tmp_path):
|
|
938
|
+
series = [_series(1.0), _series(5.0, dim="M2")]
|
|
939
|
+
expected = _write_series(tmp_path / "expected" / "series.json", series)
|
|
940
|
+
actual = _write_series(tmp_path / "actual" / "series.json", series)
|
|
941
|
+
|
|
942
|
+
validate_series_regression(expected, actual, slug="my-diag")
|
|
943
|
+
|
|
944
|
+
def test_order_insensitive(self, tmp_path):
|
|
945
|
+
a, b = _series(1.0, dim="M1"), _series(5.0, dim="M2")
|
|
946
|
+
expected = _write_series(tmp_path / "expected" / "series.json", [a, b])
|
|
947
|
+
actual = _write_series(tmp_path / "actual" / "series.json", [b, a])
|
|
948
|
+
|
|
949
|
+
validate_series_regression(expected, actual, slug="my-diag")
|
|
950
|
+
|
|
951
|
+
def test_missing_expected_is_skipped(self, tmp_path):
|
|
952
|
+
actual = _write_series(tmp_path / "actual" / "series.json", [_series(1.0)])
|
|
953
|
+
missing_expected = tmp_path / "expected" / "series.json"
|
|
954
|
+
|
|
955
|
+
# Must not raise, even though actual is non-empty.
|
|
956
|
+
validate_series_regression(missing_expected, actual, slug="my-diag")
|
|
957
|
+
|
|
958
|
+
def test_count_mismatch_raises(self, tmp_path):
|
|
959
|
+
"""A different number of series fails with a count message (Check for #703)."""
|
|
960
|
+
expected = _write_series(tmp_path / "expected" / "series.json", [])
|
|
961
|
+
actual = _write_series(tmp_path / "actual" / "series.json", [_series(1.0)])
|
|
962
|
+
|
|
963
|
+
with pytest.raises(AssertionError, match=r"produced 1 series but the committed series\.json has 0"):
|
|
964
|
+
validate_series_regression(expected, actual, slug="my-diag")
|
|
965
|
+
|
|
966
|
+
def test_value_mismatch_raises(self, tmp_path):
|
|
967
|
+
"""Same count but different values fails with a content message."""
|
|
968
|
+
expected = _write_series(tmp_path / "expected" / "series.json", [_series(1.0)])
|
|
969
|
+
actual = _write_series(tmp_path / "actual" / "series.json", [_series(2.0)])
|
|
970
|
+
|
|
971
|
+
with pytest.raises(AssertionError, match="produced series that differ"):
|
|
972
|
+
validate_series_regression(expected, actual, slug="my-diag")
|
|
973
|
+
|
|
974
|
+
def test_attribute_mismatch_raises(self, tmp_path):
|
|
975
|
+
"""Differences in series attributes are detected."""
|
|
976
|
+
expected = _write_series(
|
|
977
|
+
tmp_path / "expected" / "series.json", [_series(1.0, attrs={"caption": "old"})]
|
|
978
|
+
)
|
|
979
|
+
actual = _write_series(tmp_path / "actual" / "series.json", [_series(1.0, attrs={"caption": "new"})])
|
|
980
|
+
|
|
981
|
+
with pytest.raises(AssertionError, match="produced series that differ"):
|
|
982
|
+
validate_series_regression(expected, actual, slug="my-diag")
|
|
983
|
+
|
|
984
|
+
def test_slug_in_error_message(self, tmp_path):
|
|
985
|
+
"""The diagnostic slug is surfaced in failure messages."""
|
|
986
|
+
expected = _write_series(tmp_path / "expected" / "series.json", [])
|
|
987
|
+
actual = _write_series(tmp_path / "actual" / "series.json", [_series(1.0)])
|
|
988
|
+
|
|
989
|
+
with pytest.raises(AssertionError, match="Diagnostic some-slug"):
|
|
990
|
+
validate_series_regression(expected, actual, slug="some-slug")
|
|
991
|
+
|
|
992
|
+
def test_sanitised_path_placeholders_are_normalised(self, tmp_path):
|
|
993
|
+
"""Committed placeholders compare equal to expanded regenerated paths."""
|
|
994
|
+
expected = _write_series(
|
|
995
|
+
tmp_path / "expected" / "series.json",
|
|
996
|
+
[_series(1.0, attrs={"source_file": "<TEST_DATA_DIR>/input.nc"})],
|
|
997
|
+
)
|
|
998
|
+
actual = _write_series(
|
|
999
|
+
tmp_path / "actual" / "series.json",
|
|
1000
|
+
[_series(1.0, attrs={"source_file": f"{tmp_path}/input.nc"})],
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
validate_series_regression(
|
|
1004
|
+
expected,
|
|
1005
|
+
actual,
|
|
1006
|
+
slug="my-diag",
|
|
1007
|
+
replacements={str(tmp_path): "<TEST_DATA_DIR>"},
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
def test_nested_replacements(self, tmp_path):
|
|
1011
|
+
"""A nested output dir is substituted before its parent test-data dir."""
|
|
1012
|
+
test_data_dir = tmp_path / "test-data"
|
|
1013
|
+
output_dir = test_data_dir / "diag" / "default" / "output"
|
|
1014
|
+
output_dir.mkdir(parents=True)
|
|
1015
|
+
|
|
1016
|
+
expected = _write_series(
|
|
1017
|
+
test_data_dir / "expected" / "series.json",
|
|
1018
|
+
[_series(1.0, attrs={"plot": "<OUTPUT_DIR>/plot.png", "input": "<TEST_DATA_DIR>/in.nc"})],
|
|
1019
|
+
)
|
|
1020
|
+
actual = _write_series(
|
|
1021
|
+
output_dir / "series.json",
|
|
1022
|
+
[_series(1.0, attrs={"plot": f"{output_dir}/plot.png", "input": f"{test_data_dir}/in.nc"})],
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
validate_series_regression(
|
|
1026
|
+
expected,
|
|
1027
|
+
actual,
|
|
1028
|
+
slug="my-diag",
|
|
1029
|
+
replacements={
|
|
1030
|
+
str(output_dir): "<OUTPUT_DIR>",
|
|
1031
|
+
str(test_data_dir): "<TEST_DATA_DIR>",
|
|
1032
|
+
},
|
|
1033
|
+
)
|
|
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
|
|
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
|
|
File without changes
|
{climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/metric_values/__init__.py
RENAMED
|
File without changes
|
{climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/metric_values/typing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/src/climate_ref_core/pycmec/cv_cmip7_aft.yaml
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/pycmec/cmec_testdata/cv_sample.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{climate_ref_core-0.14.2 → climate_ref_core-0.14.4}/tests/unit/pycmec/test_controlled_vocabulary.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|