From bf33c7e4c016b64b430b03968f657a35a86fa040 Mon Sep 17 00:00:00 2001 From: Michael Wathen Date: Tue, 30 Jun 2026 15:32:41 +0100 Subject: [PATCH] Adding tests for yaml files in testcases --- .github/workflows/ersem.yml | 12 +++ testcases/check_yaml.py | 115 ++++++++++++++++++++++++ testcases/fabm-ersem-26.02-dvm-n2o.yaml | 2 +- testcases/fabm-ersem-26.02-dvm.yaml | 2 +- 4 files changed, 129 insertions(+), 2 deletions(-) create mode 100755 testcases/check_yaml.py diff --git a/.github/workflows/ersem.yml b/.github/workflows/ersem.yml index d5bb999..ffc6b29 100644 --- a/.github/workflows/ersem.yml +++ b/.github/workflows/ersem.yml @@ -10,6 +10,18 @@ on: schedule: - cron: '0 9 * * 1' jobs: + check-testcase-yaml: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Check testcase YAML files + run: | + python -m pip install PyYAML + python testcases/check_yaml.py build-pyfabm-ersem-debian: runs-on: ubuntu-latest steps: diff --git a/testcases/check_yaml.py b/testcases/check_yaml.py new file mode 100755 index 0000000..739e5c4 --- /dev/null +++ b/testcases/check_yaml.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Check that testcase YAML files can be parsed by PyYAML.""" + +import argparse +import sys +from pathlib import Path + +try: + import yaml +except ImportError: + sys.stderr.write( + "PyYAML is required. Install it with: python -m pip install PyYAML\n" + ) + sys.exit(2) + + +YAML_PATTERNS = ("*.yaml", "*.yml", "*.yaml.template", "*.yml.template") + + +def iter_yaml_files(paths): + seen = set() + + for path in paths: + path = path.resolve() + if path.is_dir(): + candidates = ( + candidate + for pattern in YAML_PATTERNS + for candidate in path.rglob(pattern) + ) + elif path.is_file(): + candidates = (path,) + else: + raise FileNotFoundError(path) + + for candidate in candidates: + if candidate.is_file() and candidate not in seen: + seen.add(candidate) + yield candidate + + +def format_yaml_error(path, error): + mark = getattr(error, "problem_mark", None) + if mark is not None: + location = f"{path}:{mark.line + 1}:{mark.column + 1}" + else: + location = str(path) + + problem = getattr(error, "problem", None) + if problem: + return f"{location}: {problem}" + return f"{location}: {error}" + + +def check_yaml(path): + try: + with path.open("r", encoding="utf-8") as stream: + yaml.safe_load(stream) + except yaml.YAMLError as error: + return format_yaml_error(path, error) + except OSError as error: + return f"{path}: {error}" + return None + + +def main(): + parser = argparse.ArgumentParser( + description="Check that testcase YAML files can be parsed by PyYAML." + ) + parser.add_argument( + "paths", + nargs="*", + type=Path, + default=[Path(__file__).resolve().parent], + help="YAML files or directories to check; defaults to this script's directory.", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Print each file as it is successfully parsed.", + ) + args = parser.parse_args() + + try: + paths = sorted(iter_yaml_files(args.paths)) + except FileNotFoundError as error: + print(f"Path does not exist: {error}", file=sys.stderr) + return 2 + + if not paths: + print("No YAML files found.", file=sys.stderr) + return 2 + + failures = [] + for path in paths: + error = check_yaml(path) + if error is None: + if args.verbose: + print(f"OK {path}") + else: + failures.append(error) + print(f"FAILED {error}", file=sys.stderr) + + checked = len(paths) + if failures: + print(f"Checked {checked} YAML files: {len(failures)} failed.", file=sys.stderr) + return 1 + + print(f"Checked {checked} YAML files: all OK.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/testcases/fabm-ersem-26.02-dvm-n2o.yaml b/testcases/fabm-ersem-26.02-dvm-n2o.yaml index 72b4819..9574900 100755 --- a/testcases/fabm-ersem-26.02-dvm-n2o.yaml +++ b/testcases/fabm-ersem-26.02-dvm-n2o.yaml @@ -273,7 +273,7 @@ instances: q10: 2.0 # Q_10 temperature coefficient (-) srs: 0.04 # specific rest respiration at reference temperature (1/d) pu_ea: 0.05 # excreted fraction of primary production (-) - exulim: 0.3 # excreted fraction of primary production due to nutrient stress,maximum=1._rk-self%pu_ea + exulim: 0.3 # excreted fraction of primary production due to nutrient stress,maximum=1._rk-self%pu_ea xqcpx: 1.3 # threshold for phosphorus limitation, relative to minimum ratio,default=2.0_rk xqcnx: 1.3 # threshold for nitrogen limitation, relative to minimum ratio, default=2.0_rk pu_ra: 0.2 # respired fraction of primary production (-) diff --git a/testcases/fabm-ersem-26.02-dvm.yaml b/testcases/fabm-ersem-26.02-dvm.yaml index 22495bd..2f95da6 100755 --- a/testcases/fabm-ersem-26.02-dvm.yaml +++ b/testcases/fabm-ersem-26.02-dvm.yaml @@ -263,7 +263,7 @@ instances: q10: 2.0 # Q_10 temperature coefficient (-) srs: 0.04 # specific rest respiration at reference temperature (1/d) pu_ea: 0.05 # excreted fraction of primary production (-) - exulim: 0.3 # excreted fraction of primary production due to nutrient stress,maximum=1._rk-self%pu_ea + exulim: 0.3 # excreted fraction of primary production due to nutrient stress,maximum=1._rk-self%pu_ea xqcpx: 1.3 # threshold for phosphorus limitation, relative to minimum ratio,default=2.0_rk xqcnx: 1.3 # threshold for nitrogen limitation, relative to minimum ratio, default=2.0_rk pu_ra: 0.2 # respired fraction of primary production (-)