From cc7c6761914a88f5e35e448ee051d974ddfd5e91 Mon Sep 17 00:00:00 2001 From: EdHone Date: Mon, 8 Dec 2025 16:01:36 +0000 Subject: [PATCH 1/7] Add test framework for iodef.xml files --- .../build/testframework/xiostest.py | 7 ++++ .../lfric_xios_context_test.py | 2 ++ .../lfric_xios_temporal_test.py | 34 +++++++++++++++++++ .../lfric_xios_time_read_test.py | 1 + .../integration-test/resources/iodef.xml | 19 +++++++++++ .../resources/iodef_temporal.xml | 31 +++++++++++++++++ 6 files changed, 94 insertions(+) create mode 100644 components/lfric-xios/integration-test/resources/iodef.xml create mode 100644 components/lfric-xios/integration-test/resources/iodef_temporal.xml diff --git a/components/lfric-xios/build/testframework/xiostest.py b/components/lfric-xios/build/testframework/xiostest.py index fddbc1791..f4846086f 100644 --- a/components/lfric-xios/build/testframework/xiostest.py +++ b/components/lfric-xios/build/testframework/xiostest.py @@ -8,6 +8,7 @@ import subprocess from pathlib import Path import sys +import shutil from typing import List from testframework import MpiTest @@ -38,6 +39,12 @@ def gen_data(self, source: Path, dest: Path): if proc.returncode != 0: raise Exception("Test data generation failed:\n" + f"{err}") + def use_iodef(self, iodef_source: Path): + """ + Copy an iodef file to the working directory. + """ + shutil.copy(iodef_source, Path(os.getcwd()) / "iodef.xml") + def gen_config(self, config_source: Path, config_out: Path, new_config: dict): """ Create an LFRic configuration namelist. diff --git a/components/lfric-xios/integration-test/lfric_xios_context_test.py b/components/lfric-xios/integration-test/lfric_xios_context_test.py index 32a504db0..c47fd9d72 100755 --- a/components/lfric-xios/integration-test/lfric_xios_context_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_context_test.py @@ -11,6 +11,7 @@ from testframework import TestEngine, TestFailed from xiostest import LFRicXiosTest +from pathlib import Path import sys ############################################################################### @@ -21,6 +22,7 @@ class LfricXiosContextTest(LFRicXiosTest): def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/context.nml"], processes=1) + self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_test.py b/components/lfric-xios/integration-test/lfric_xios_temporal_test.py index 21bb48e1d..cc18b5d6e 100755 --- a/components/lfric-xios/integration-test/lfric_xios_temporal_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_test.py @@ -29,6 +29,7 @@ def __init__(self): self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) self.gen_config( Path("resources/configs/non_cyclic_base.nml"), Path("resources/configs/non_cyclic_full.nml"), {} ) + self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ @@ -62,6 +63,7 @@ def __init__(self): self.gen_config( Path("resources/configs/non_cyclic_base.nml"), Path("resources/configs/non_cyclic_high_freq.nml"), {"dt":"10.0"} ) + self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ @@ -95,6 +97,7 @@ def __init__(self): self.gen_config( Path("resources/configs/non_cyclic_base.nml"), Path("resources/configs/non_cyclic_mid.nml"), {'calendar_start':"'2024-01-01 15:01:00'"} ) + self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ @@ -128,6 +131,7 @@ def __init__(self): Path("resources/configs/non_cyclic_future.nml"), {'calendar_start':"'2024-01-01 10:00:00'", 'calendar_origin':"'2024-01-01 10:00:00'"} ) + self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ @@ -161,6 +165,7 @@ def __init__(self): Path("resources/configs/non_cyclic_past.nml"), {'calendar_start':"'2024-02-01 10:00:00'", 'calendar_origin':"'2024-02-01 10:00:00'"} ) + self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ @@ -180,6 +185,34 @@ def test(self, returncode: int, out: str, err: str): return "Expected error for past non-cyclic data reading..." +class LfricXiosNonCyclicFromIodef(LFRicXiosTest): # pylint: disable=too-few-public-methods + """ + Tests the LFRic-XIOS reading for non-cyclic data in the future (expected failure) + """ + + def __init__(self): + super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) + test_data_dir = Path(Path.cwd(), 'resources/data') + Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) + self.gen_config( Path("resources/configs/non_cyclic_base.nml"), + Path("resources/configs/non_cyclic_full.nml"), {} ) + self.use_iodef(Path("resources/iodef.xml")) + + + def test(self, returncode: int, out: str, err: str): + """ + Test the output of the context test + """ + + if returncode != 0: + raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + + f"stderr:\n" + + f"{err}") + + return "From iodef" + + ############################################################################## @@ -189,3 +222,4 @@ def test(self, returncode: int, out: str, err: str): TestEngine.run(LfricXiosPartialNonCyclicTest()) TestEngine.run(LfricXiosNonCyclicFutureTest()) TestEngine.run(LfricXiosNonCyclicPastTest()) + TestEngine.run(LfricXiosNonCyclicFromIodef()) diff --git a/components/lfric-xios/integration-test/lfric_xios_time_read_test.py b/components/lfric-xios/integration-test/lfric_xios_time_read_test.py index d8b963f9b..6e646db25 100755 --- a/components/lfric-xios/integration-test/lfric_xios_time_read_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_time_read_test.py @@ -27,6 +27,7 @@ def __init__(self, nprocs: int): Path('lfric_xios_time_read_data.nc').unlink(missing_ok=True) self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_time_read_data.nc')) self.nprocs = nprocs + self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ diff --git a/components/lfric-xios/integration-test/resources/iodef.xml b/components/lfric-xios/integration-test/resources/iodef.xml new file mode 100644 index 000000000..babba32a1 --- /dev/null +++ b/components/lfric-xios/integration-test/resources/iodef.xml @@ -0,0 +1,19 @@ + + + + + + + performance + 1.0 + + + + true + 50 + true + + + + + diff --git a/components/lfric-xios/integration-test/resources/iodef_temporal.xml b/components/lfric-xios/integration-test/resources/iodef_temporal.xml new file mode 100644 index 000000000..2b0f197de --- /dev/null +++ b/components/lfric-xios/integration-test/resources/iodef_temporal.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + performance + 1.0 + + + + true + 50 + true + + + + + From c065ad9e014ac930ce7ef8867141c11a2d44eb7f Mon Sep 17 00:00:00 2001 From: EdHone Date: Tue, 9 Dec 2025 11:40:17 +0000 Subject: [PATCH 2/7] New test and tweak for file functionality --- .../lfric_xios_temporal_iodef_test.f90 | 86 ++++++++++++ .../lfric_xios_temporal_iodef_test.py | 127 ++++++++++++++++++ .../lfric_xios_temporal_test.py | 33 +---- .../lfric_xios_time_read_test.py | 1 - .../resources/iodef_temporal.xml | 5 +- .../lfric-xios/source/lfric_xios_file_mod.f90 | 5 +- 6 files changed, 220 insertions(+), 37 deletions(-) create mode 100644 components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.f90 create mode 100755 components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.f90 b/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.f90 new file mode 100644 index 000000000..a2b8cac79 --- /dev/null +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.f90 @@ -0,0 +1,86 @@ +!----------------------------------------------------------------------------- +! (C) Crown copyright 2025 Met Office. All rights reserved. +! The file LICENCE, distributed with this code, contains details of the terms +! under which the code may be used. +!----------------------------------------------------------------------------- + +! Tests the LFRic-XIOS temporal reading functionality using iodef file configuration. +! Correct behaviour is to read only the minimal required time-entries from +! input file at the correct times. The validity of the data written from this +! test is checked against the input data in the python part of the test. +program lfric_xios_temporal_iodef_test + + use constants_mod, only: r_def + use event_mod, only: event_action + use event_actor_mod, only: event_actor_type + use field_mod, only: field_type, field_proxy_type + use file_mod, only: FILE_MODE_READ, FILE_MODE_WRITE + use io_context_mod, only: callback_clock_arg + use lfric_xios_action_mod, only: advance + use lfric_xios_context_mod, only: lfric_xios_context_type + use lfric_xios_driver_mod, only: lfric_xios_initialise, lfric_xios_finalise + use lfric_xios_file_mod, only: lfric_xios_file_type, OPERATION_TIMESERIES + use linked_list_mod, only: linked_list_type + use log_mod, only: log_event, log_level_info + use test_db_mod, only: test_db_type + + implicit none + + type(test_db_type) :: test_db + type(lfric_xios_context_type), target, allocatable :: io_context + + procedure(callback_clock_arg), pointer :: before_close + type(linked_list_type), pointer :: file_list + class(event_actor_type), pointer :: context_actor + procedure(event_action), pointer :: context_advance + type(field_type), pointer :: rfield + type(field_proxy_type) :: rproxy + + call test_db%initialise() + call lfric_xios_initialise( "test", test_db%comm, .false. ) + + ! =============================== Start test ================================ + + allocate(io_context) + call io_context%initialise( "test_io_context", 1, 10 ) + + file_list => io_context%get_filelist() + call file_list%insert_item( lfric_xios_file_type( "lfric_xios_temporal_input", & + xios_id="lfric_xios_temporal_input", & + io_mode=FILE_MODE_READ, & + operation=OPERATION_TIMESERIES, & + fields_in_file=test_db%temporal_fields ) ) + call file_list%insert_item( lfric_xios_file_type( "lfric_xios_temporal_output", & + xios_id="lfric_xios_temporal_output", & + io_mode=FILE_MODE_WRITE, & + operation=OPERATION_TIMESERIES, & + freq=1, & + fields_in_file=test_db%temporal_fields ) ) + + before_close => null() + call io_context%initialise_xios_context( test_db%comm, & + test_db%chi, test_db%panel_id, & + test_db%clock, test_db%calendar, & + before_close ) + + + context_advance => advance + context_actor => io_context + call test_db%clock%add_event( context_advance, context_actor ) + call io_context%set_active(.true.) + + do while (test_db%clock%tick()) + call test_db%temporal_fields%get_field("temporal_field", rfield) + rproxy = rfield%get_proxy() + call log_event("Valid data for this TS:", log_level_info) + print*,rproxy%data(1) + end do + + deallocate(io_context) + + ! ============================== Finish test ================================= + + call lfric_xios_finalise() + call test_db%finalise() + +end program lfric_xios_temporal_iodef_test diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py b/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py new file mode 100755 index 000000000..04d919762 --- /dev/null +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +############################################################################## +# (C) Crown copyright 2025 Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +############################################################################## +""" +A set of tests which exercise the temporal reading functionality provided by +the LFRic-XIOS component. For these tests the file is configured mainly via +the iodef.xml file, rather than the fortran API. +The tests cover the reading of a piece of non-cyclic temporal data with data +points ranging from 15:01 to 15:10 in 10 1-minute intervals. The model start +time is changed to change how the model interacts with the data. +""" +from testframework import TestEngine, TestFailed +from xiostest import LFRicXiosTest +from pathlib import Path +import sys + +############################################################################### +class LfricXiosFullNonCyclicIodefTest(LFRicXiosTest): # pylint: disable=too-few-public-methods + """ + Tests the LFRic-XIOS temporal reading functionality for a full set of non-cyclic data + """ + + def __init__(self): + super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) + test_data_dir = Path(Path.cwd(), 'resources/data') + Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) + self.gen_config( Path("resources/configs/non_cyclic_base.nml"), + Path("resources/configs/non_cyclic_full.nml"), {} ) + self.use_iodef(Path("resources/iodef_temporal.xml")) + + def test(self, returncode: int, out: str, err: str): + """ + Test the output of the context test + """ + + if returncode != 0: + print(out) + raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + + f"stderr:\n" + + f"{err}") + if not self.nc_data_match(Path('lfric_xios_temporal_input.nc'), + Path('lfric_xios_temporal_output.nc'), + 'temporal_field'): + raise TestFailed("Output data does not match input data for same time values") + + return "Reading full set of non-cylic data okay..." + + +class LfricXiosFullNonCyclicIodefHighFreqTest(LFRicXiosTest): # pylint: disable=too-few-public-methods + """ + Tests the LFRic-XIOS temporal reading functionality for a full set of + non-cyclic data at hieher model frequency + """ + + def __init__(self): + super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) + test_data_dir = Path(Path.cwd(), 'resources/data') + Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) + self.gen_config( Path("resources/configs/non_cyclic_base.nml"), + Path("resources/configs/non_cyclic_full.nml"), + {"dt": 10.0, + "timestep_end": 60} ) + self.use_iodef(Path("resources/iodef_temporal.xml")) + + def test(self, returncode: int, out: str, err: str): + """ + Test the output of the context test + """ + + if returncode != 0: + print(out) + raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + + f"stderr:\n" + + f"{err}") + if not self.nc_data_match(Path('lfric_xios_temporal_input.nc'), + Path('lfric_xios_temporal_output.nc'), + 'temporal_field'): + raise TestFailed("Output data does not match input data for same time values") + + return "Reading full set of non-cylic data okay..." + + +class LfricXiosFullNonCyclicIodefNoFreqTest(LFRicXiosTest): # pylint: disable=too-few-public-methods + """ + Tests the error handling for the case where there is no frequency set in either + the iodef or the fortran configuration. + """ + + def __init__(self): + super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) + test_data_dir = Path(Path.cwd(), 'resources/data') + Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) + self.gen_config( Path("resources/configs/non_cyclic_base.nml"), + Path("resources/configs/non_cyclic_full.nml"), {} ) + self.use_iodef(Path("resources/iodef.xml")) + + def test(self, returncode: int, out: str, err: str): + """ + Test the output of the context test + """ + + expected_xios_err = 'In file "type_impl.hpp", function "void xios::CType::_checkEmpty() const [with T = xios::CDuration]", line 210 -> Data is not initialized' + + if returncode == 134: + if self.xios_err[0].contents.strip() == expected_xios_err: + return "Expected failure of test executable due to missing frequency setting." + else: + raise TestFailed("Test executable failed, but with unexpected error message.") + elif returncode == 0: + raise TestFailed("Test executable succeeded unexpectedly despite missing frequency setting.") + else: + raise TestFailed("Test executable failed with unexpected return code.") + + + + +############################################################################## +if __name__ == "__main__": + TestEngine.run(LfricXiosFullNonCyclicIodefTest()) + TestEngine.run(LfricXiosFullNonCyclicIodefHighFreqTest()) + TestEngine.run(LfricXiosFullNonCyclicIodefNoFreqTest()) \ No newline at end of file diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_test.py b/components/lfric-xios/integration-test/lfric_xios_temporal_test.py index cc18b5d6e..376b4136d 100755 --- a/components/lfric-xios/integration-test/lfric_xios_temporal_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_test.py @@ -185,41 +185,10 @@ def test(self, returncode: int, out: str, err: str): return "Expected error for past non-cyclic data reading..." -class LfricXiosNonCyclicFromIodef(LFRicXiosTest): # pylint: disable=too-few-public-methods - """ - Tests the LFRic-XIOS reading for non-cyclic data in the future (expected failure) - """ - - def __init__(self): - super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) - test_data_dir = Path(Path.cwd(), 'resources/data') - Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) - self.gen_config( Path("resources/configs/non_cyclic_base.nml"), - Path("resources/configs/non_cyclic_full.nml"), {} ) - self.use_iodef(Path("resources/iodef.xml")) - - - def test(self, returncode: int, out: str, err: str): - """ - Test the output of the context test - """ - - if returncode != 0: - raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + - f"stderr:\n" + - f"{err}") - - return "From iodef" - - - - ############################################################################## if __name__ == "__main__": TestEngine.run(LfricXiosFullNonCyclicTest()) TestEngine.run(LfricXiosNonCyclicHighFreqTest()) TestEngine.run(LfricXiosPartialNonCyclicTest()) TestEngine.run(LfricXiosNonCyclicFutureTest()) - TestEngine.run(LfricXiosNonCyclicPastTest()) - TestEngine.run(LfricXiosNonCyclicFromIodef()) + TestEngine.run(LfricXiosNonCyclicPastTest()) \ No newline at end of file diff --git a/components/lfric-xios/integration-test/lfric_xios_time_read_test.py b/components/lfric-xios/integration-test/lfric_xios_time_read_test.py index 6e646db25..f9ee0172f 100755 --- a/components/lfric-xios/integration-test/lfric_xios_time_read_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_time_read_test.py @@ -10,7 +10,6 @@ """ from testframework import TestEngine, TestFailed from xiostest import LFRicXiosTest -import subprocess import sys from pathlib import Path diff --git a/components/lfric-xios/integration-test/resources/iodef_temporal.xml b/components/lfric-xios/integration-test/resources/iodef_temporal.xml index 2b0f197de..d2e310a29 100644 --- a/components/lfric-xios/integration-test/resources/iodef_temporal.xml +++ b/components/lfric-xios/integration-test/resources/iodef_temporal.xml @@ -4,10 +4,9 @@ + type="one_file"> - + diff --git a/components/lfric-xios/source/lfric_xios_file_mod.f90 b/components/lfric-xios/source/lfric_xios_file_mod.f90 index cc1e9a820..3de76edcc 100644 --- a/components/lfric-xios/source/lfric_xios_file_mod.f90 +++ b/components/lfric-xios/source/lfric_xios_file_mod.f90 @@ -389,7 +389,10 @@ subroutine register_with_context(self) call xios_get_timestep(timestep_duration) if (.not. self%freq_ts == undef_freq) then self%frequency = self%freq_ts * timestep_duration - call xios_set_attr( self%handle, output_freq=self%frequency ) + call xios_set_attr(self%handle, output_freq=self%frequency) + else + ! If frequency is uninitialised, get it from XIOS + call xios_get_file_attr(self%xios_id, output_freq=self%frequency) end if ! Set the date of the first operation From 8705e3a76defe8fac23f371f601d89b62c06d133 Mon Sep 17 00:00:00 2001 From: EdHone Date: Mon, 5 Jan 2026 16:45:32 +0000 Subject: [PATCH 3/7] fix tests and gitignore --- .gitignore | 2 ++ .../integration-test/lfric_xios_temporal_iodef_test.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 0837ac81d..164a542f5 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,5 @@ infrastructure/**/bin infrastructure/**/working infrastructure/**/test infrastructure/**/documents +science/**/bin +science/**/working diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py b/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py index 04d919762..4c4546da8 100755 --- a/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py @@ -25,6 +25,7 @@ class LfricXiosFullNonCyclicIodefTest(LFRicXiosTest): # pylint: disable=too-few def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) + print(Path.cwd()) test_data_dir = Path(Path.cwd(), 'resources/data') Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) @@ -93,6 +94,7 @@ class LfricXiosFullNonCyclicIodefNoFreqTest(LFRicXiosTest): # pylint: disable=t def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) + print(Path.cwd()) test_data_dir = Path(Path.cwd(), 'resources/data') Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) @@ -118,8 +120,6 @@ def test(self, returncode: int, out: str, err: str): raise TestFailed("Test executable failed with unexpected return code.") - - ############################################################################## if __name__ == "__main__": TestEngine.run(LfricXiosFullNonCyclicIodefTest()) From b76e3046be4db3584a2ac137c8790b700623e630 Mon Sep 17 00:00:00 2001 From: EdHone Date: Mon, 5 Jan 2026 16:42:22 +0000 Subject: [PATCH 4/7] Remove original iodef.xml --- .../lfric-xios/integration-test/iodef.xml | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 components/lfric-xios/integration-test/iodef.xml diff --git a/components/lfric-xios/integration-test/iodef.xml b/components/lfric-xios/integration-test/iodef.xml deleted file mode 100644 index babba32a1..000000000 --- a/components/lfric-xios/integration-test/iodef.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - performance - 1.0 - - - - true - 50 - true - - - - - From 21f83025fff1674c5e3a19f8320e26d801ce0a61 Mon Sep 17 00:00:00 2001 From: EdHone Date: Wed, 7 Jan 2026 16:16:03 +0000 Subject: [PATCH 5/7] Test tweaks) --- .../build/testframework/xiostest.py | 29 ++++++++++++++----- .../lfric_xios_context_test.py | 1 - .../lfric_xios_temporal_iodef_test.py | 9 ++---- .../lfric_xios_temporal_test.py | 5 ---- .../lfric_xios_time_read_test.py | 1 - 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/components/lfric-xios/build/testframework/xiostest.py b/components/lfric-xios/build/testframework/xiostest.py index f4846086f..b12051a03 100644 --- a/components/lfric-xios/build/testframework/xiostest.py +++ b/components/lfric-xios/build/testframework/xiostest.py @@ -21,7 +21,12 @@ class LFRicXiosTest(MpiTest): Base for LFRic-XIOS integration tests. """ - def __init__(self, command=sys.argv[1], processes=1): + def __init__(self, command=sys.argv[1], processes=1, iodef_file=None): + if iodef_file is None: + self.iodef_file = "iodef.xml" + else: + self.iodef_file = iodef_file + super().__init__(command, processes) self.xios_out: List[XiosOutput] = [] self.xios_err: List[XiosOutput] = [] @@ -39,12 +44,6 @@ def gen_data(self, source: Path, dest: Path): if proc.returncode != 0: raise Exception("Test data generation failed:\n" + f"{err}") - def use_iodef(self, iodef_source: Path): - """ - Copy an iodef file to the working directory. - """ - shutil.copy(iodef_source, Path(os.getcwd()) / "iodef.xml") - def gen_config(self, config_source: Path, config_out: Path, new_config: dict): """ Create an LFRic configuration namelist. @@ -60,7 +59,21 @@ def gen_config(self, config_source: Path, config_out: Path, new_config: dict): f = open(config_out, "w") for line in config: f.write(line) - f.close() + f.close() + + + def performTest(self): + """ + Removes any old log files and runs the executable. + """ + + # Handle iodef file + if os.path.exists(self.iodef_file): + os.remove(self.iodef_file) + shutil.copy(Path(os.getcwd()) / "resources" / self.iodef_file, Path(os.getcwd()) / "iodef.xml") + + return super().performTest() + def nc_kgo_check(self, output: Path, kgo: Path): """ diff --git a/components/lfric-xios/integration-test/lfric_xios_context_test.py b/components/lfric-xios/integration-test/lfric_xios_context_test.py index c47fd9d72..7d22de1f2 100755 --- a/components/lfric-xios/integration-test/lfric_xios_context_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_context_test.py @@ -22,7 +22,6 @@ class LfricXiosContextTest(LFRicXiosTest): def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/context.nml"], processes=1) - self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py b/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py index 4c4546da8..c3396b357 100755 --- a/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py @@ -24,14 +24,12 @@ class LfricXiosFullNonCyclicIodefTest(LFRicXiosTest): # pylint: disable=too-few """ def __init__(self): - super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) - print(Path.cwd()) + super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1, iodef_file="iodef_temporal.xml") test_data_dir = Path(Path.cwd(), 'resources/data') Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) self.gen_config( Path("resources/configs/non_cyclic_base.nml"), Path("resources/configs/non_cyclic_full.nml"), {} ) - self.use_iodef(Path("resources/iodef_temporal.xml")) def test(self, returncode: int, out: str, err: str): """ @@ -58,7 +56,7 @@ class LfricXiosFullNonCyclicIodefHighFreqTest(LFRicXiosTest): # pylint: disable """ def __init__(self): - super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) + super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1, iodef_file="iodef_temporal.xml") test_data_dir = Path(Path.cwd(), 'resources/data') Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) @@ -66,7 +64,6 @@ def __init__(self): Path("resources/configs/non_cyclic_full.nml"), {"dt": 10.0, "timestep_end": 60} ) - self.use_iodef(Path("resources/iodef_temporal.xml")) def test(self, returncode: int, out: str, err: str): """ @@ -94,13 +91,11 @@ class LfricXiosFullNonCyclicIodefNoFreqTest(LFRicXiosTest): # pylint: disable=t def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) - print(Path.cwd()) test_data_dir = Path(Path.cwd(), 'resources/data') Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) self.gen_config( Path("resources/configs/non_cyclic_base.nml"), Path("resources/configs/non_cyclic_full.nml"), {} ) - self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_test.py b/components/lfric-xios/integration-test/lfric_xios_temporal_test.py index 376b4136d..d94fd8a5f 100755 --- a/components/lfric-xios/integration-test/lfric_xios_temporal_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_test.py @@ -29,7 +29,6 @@ def __init__(self): self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) self.gen_config( Path("resources/configs/non_cyclic_base.nml"), Path("resources/configs/non_cyclic_full.nml"), {} ) - self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ @@ -63,7 +62,6 @@ def __init__(self): self.gen_config( Path("resources/configs/non_cyclic_base.nml"), Path("resources/configs/non_cyclic_high_freq.nml"), {"dt":"10.0"} ) - self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ @@ -97,7 +95,6 @@ def __init__(self): self.gen_config( Path("resources/configs/non_cyclic_base.nml"), Path("resources/configs/non_cyclic_mid.nml"), {'calendar_start':"'2024-01-01 15:01:00'"} ) - self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ @@ -131,7 +128,6 @@ def __init__(self): Path("resources/configs/non_cyclic_future.nml"), {'calendar_start':"'2024-01-01 10:00:00'", 'calendar_origin':"'2024-01-01 10:00:00'"} ) - self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ @@ -165,7 +161,6 @@ def __init__(self): Path("resources/configs/non_cyclic_past.nml"), {'calendar_start':"'2024-02-01 10:00:00'", 'calendar_origin':"'2024-02-01 10:00:00'"} ) - self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ diff --git a/components/lfric-xios/integration-test/lfric_xios_time_read_test.py b/components/lfric-xios/integration-test/lfric_xios_time_read_test.py index f9ee0172f..93f2fea64 100755 --- a/components/lfric-xios/integration-test/lfric_xios_time_read_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_time_read_test.py @@ -26,7 +26,6 @@ def __init__(self, nprocs: int): Path('lfric_xios_time_read_data.nc').unlink(missing_ok=True) self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_time_read_data.nc')) self.nprocs = nprocs - self.use_iodef(Path("resources/iodef.xml")) def test(self, returncode: int, out: str, err: str): """ From 8b7e7403cffbe96975568d6ae99e341bd13f6086 Mon Sep 17 00:00:00 2001 From: EdHone Date: Fri, 9 Jan 2026 15:08:49 +0000 Subject: [PATCH 6/7] Make testing framework thread-safe --- .../build/testframework/xiostest.py | 25 ++++++- .../lfric_xios_temporal_iodef_test.py | 38 +++++------ .../lfric_xios_temporal_test.py | 66 +++++++++---------- .../lfric_xios_time_read_test.f90 | 2 +- .../lfric_xios_time_read_test.py | 6 +- 5 files changed, 79 insertions(+), 58 deletions(-) diff --git a/components/lfric-xios/build/testframework/xiostest.py b/components/lfric-xios/build/testframework/xiostest.py index b12051a03..07ee69909 100644 --- a/components/lfric-xios/build/testframework/xiostest.py +++ b/components/lfric-xios/build/testframework/xiostest.py @@ -27,10 +27,28 @@ def __init__(self, command=sys.argv[1], processes=1, iodef_file=None): else: self.iodef_file = iodef_file - super().__init__(command, processes) self.xios_out: List[XiosOutput] = [] self.xios_err: List[XiosOutput] = [] + # Setup test working directory + self.test_top_level: Path = Path(os.getcwd()) + self.resources_dir: Path = self.test_top_level / "resources" + self.test_working_dir: Path = self.test_top_level / "working" / type(self).__name__ + + # Replace resource path with absolute path in command + for i in range(len(command)): + if "resources/" in command[i]: + command[i] = command[i].replace("resources/", str(self.resources_dir) + "/") + + super().__init__(command, processes) + + if not os.path.exists(self.test_working_dir): + os.makedirs(self.test_working_dir) + + # Change to test working directory + os.chdir(self.test_working_dir) + + def gen_data(self, source: Path, dest: Path): """ Create input data files from CDL formatted text. @@ -70,7 +88,7 @@ def performTest(self): # Handle iodef file if os.path.exists(self.iodef_file): os.remove(self.iodef_file) - shutil.copy(Path(os.getcwd()) / "resources" / self.iodef_file, Path(os.getcwd()) / "iodef.xml") + shutil.copy(self.resources_dir / self.iodef_file, self.test_working_dir / "iodef.xml") return super().performTest() @@ -115,6 +133,9 @@ def post_execution(self, return_code): self.xios_out.append(XiosOutput(f"xios_client_{proc}.out")) self.xios_err.append(XiosOutput(f"xios_client_{proc}.err")) + # Return to top level directory + os.chdir(self.test_top_level) + class XiosOutput: """ diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py b/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py index c3396b357..828a3bfd8 100755 --- a/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py @@ -25,11 +25,11 @@ class LfricXiosFullNonCyclicIodefTest(LFRicXiosTest): # pylint: disable=too-few def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1, iodef_file="iodef_temporal.xml") - test_data_dir = Path(Path.cwd(), 'resources/data') - Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) - self.gen_config( Path("resources/configs/non_cyclic_base.nml"), - Path("resources/configs/non_cyclic_full.nml"), {} ) + test_data_dir = Path(self.resources_dir, 'data') + Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) + self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), + Path(self.resources_dir, "configs/non_cyclic_full.nml"), {} ) def test(self, returncode: int, out: str, err: str): """ @@ -41,8 +41,8 @@ def test(self, returncode: int, out: str, err: str): raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + f"stderr:\n" + f"{err}") - if not self.nc_data_match(Path('lfric_xios_temporal_input.nc'), - Path('lfric_xios_temporal_output.nc'), + if not self.nc_data_match(Path(self.test_working_dir, 'lfric_xios_temporal_input.nc'), + Path(self.test_working_dir, 'lfric_xios_temporal_output.nc'), 'temporal_field'): raise TestFailed("Output data does not match input data for same time values") @@ -57,11 +57,11 @@ class LfricXiosFullNonCyclicIodefHighFreqTest(LFRicXiosTest): # pylint: disable def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1, iodef_file="iodef_temporal.xml") - test_data_dir = Path(Path.cwd(), 'resources/data') - Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) - self.gen_config( Path("resources/configs/non_cyclic_base.nml"), - Path("resources/configs/non_cyclic_full.nml"), + test_data_dir = Path(self.resources_dir, 'data') + Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) + self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), + Path(self.resources_dir, "configs/non_cyclic_full.nml"), {"dt": 10.0, "timestep_end": 60} ) @@ -75,8 +75,8 @@ def test(self, returncode: int, out: str, err: str): raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + f"stderr:\n" + f"{err}") - if not self.nc_data_match(Path('lfric_xios_temporal_input.nc'), - Path('lfric_xios_temporal_output.nc'), + if not self.nc_data_match(Path(self.test_working_dir, 'lfric_xios_temporal_input.nc'), + Path(self.test_working_dir, 'lfric_xios_temporal_output.nc'), 'temporal_field'): raise TestFailed("Output data does not match input data for same time values") @@ -91,11 +91,11 @@ class LfricXiosFullNonCyclicIodefNoFreqTest(LFRicXiosTest): # pylint: disable=t def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) - test_data_dir = Path(Path.cwd(), 'resources/data') - Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) - self.gen_config( Path("resources/configs/non_cyclic_base.nml"), - Path("resources/configs/non_cyclic_full.nml"), {} ) + test_data_dir = Path(self.resources_dir, 'data') + Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) + self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), + Path(self.resources_dir, "configs/non_cyclic_full.nml"), {} ) def test(self, returncode: int, out: str, err: str): """ diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_test.py b/components/lfric-xios/integration-test/lfric_xios_temporal_test.py index d94fd8a5f..1e96f4740 100755 --- a/components/lfric-xios/integration-test/lfric_xios_temporal_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_test.py @@ -24,11 +24,11 @@ class LfricXiosFullNonCyclicTest(LFRicXiosTest): # pylint: disable=too-few-publ def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) - test_data_dir = Path(Path.cwd(), 'resources/data') - Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) - self.gen_config( Path("resources/configs/non_cyclic_base.nml"), - Path("resources/configs/non_cyclic_full.nml"), {} ) + test_data_dir = Path(self.resources_dir, 'data') + Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) + self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), + Path(self.resources_dir, "configs/non_cyclic_full.nml"), {} ) def test(self, returncode: int, out: str, err: str): """ @@ -40,8 +40,8 @@ def test(self, returncode: int, out: str, err: str): raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + f"stderr:\n" + f"{err}") - if not self.nc_data_match(Path('lfric_xios_temporal_input.nc'), - Path('lfric_xios_temporal_output.nc'), + if not self.nc_data_match(Path(self.test_working_dir, 'lfric_xios_temporal_input.nc'), + Path(self.test_working_dir, 'lfric_xios_temporal_output.nc'), 'temporal_field'): raise TestFailed("Output data does not match input data for same time values") @@ -56,11 +56,11 @@ class LfricXiosNonCyclicHighFreqTest(LFRicXiosTest): # pylint: disable=too-few- def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_high_freq.nml"], processes=1) - test_data_dir = Path(Path.cwd(), 'resources/data') - Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) - self.gen_config( Path("resources/configs/non_cyclic_base.nml"), - Path("resources/configs/non_cyclic_high_freq.nml"), + test_data_dir = Path(self.resources_dir, 'data') + Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) + self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), + Path(self.resources_dir, "configs/non_cyclic_high_freq.nml"), {"dt":"10.0"} ) def test(self, returncode: int, out: str, err: str): @@ -70,11 +70,11 @@ def test(self, returncode: int, out: str, err: str): if returncode != 0: print(out) - raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + + raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + f"stderr:\n" + f"{err}") - if not self.nc_data_match(Path('lfric_xios_temporal_input.nc'), - Path('lfric_xios_temporal_output.nc'), + if not self.nc_data_match(Path(self.test_working_dir, 'lfric_xios_temporal_input.nc'), + Path(self.test_working_dir, 'lfric_xios_temporal_output.nc'), 'temporal_field'): raise TestFailed("Output data does not match input data for same time values") @@ -89,11 +89,11 @@ class LfricXiosPartialNonCyclicTest(LFRicXiosTest): # pylint: disable=too-few-p def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_mid.nml"], processes=1) - test_data_dir = Path(Path.cwd(), 'resources/data') - Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) - self.gen_config( Path("resources/configs/non_cyclic_base.nml"), - Path("resources/configs/non_cyclic_mid.nml"), + test_data_dir = Path(self.resources_dir, 'data') + Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) + self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), + Path(self.resources_dir, "configs/non_cyclic_mid.nml"), {'calendar_start':"'2024-01-01 15:01:00'"} ) def test(self, returncode: int, out: str, err: str): @@ -102,12 +102,12 @@ def test(self, returncode: int, out: str, err: str): """ if returncode != 0: - raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + + raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + f"stderr:\n" + f"{err}") - if not self.nc_data_match(Path('lfric_xios_temporal_input.nc'), - Path('lfric_xios_temporal_output.nc'), + if not self.nc_data_match(Path(self.test_working_dir, 'lfric_xios_temporal_input.nc'), + Path(self.test_working_dir, 'lfric_xios_temporal_output.nc'), 'temporal_field'): raise TestFailed("Output data does not match input data for same time values") @@ -121,11 +121,11 @@ class LfricXiosNonCyclicFutureTest(LFRicXiosTest): # pylint: disable=too-few-pu def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_future.nml"], processes=1) - test_data_dir = Path(Path.cwd(), 'resources/data') - Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) - self.gen_config( Path("resources/configs/non_cyclic_base.nml"), - Path("resources/configs/non_cyclic_future.nml"), + test_data_dir = Path(self.resources_dir, 'data') + Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) + self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), + Path(self.resources_dir, "configs/non_cyclic_future.nml"), {'calendar_start':"'2024-01-01 10:00:00'", 'calendar_origin':"'2024-01-01 10:00:00'"} ) @@ -154,11 +154,11 @@ class LfricXiosNonCyclicPastTest(LFRicXiosTest): # pylint: disable=too-few-publ def __init__(self): super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_past.nml"], processes=1) - test_data_dir = Path(Path.cwd(), 'resources/data') - Path('lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_temporal_input.nc')) - self.gen_config( Path("resources/configs/non_cyclic_base.nml"), - Path("resources/configs/non_cyclic_past.nml"), + test_data_dir = Path(self.resources_dir, 'data') + Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) + self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), + Path(self.resources_dir, "configs/non_cyclic_past.nml"), {'calendar_start':"'2024-02-01 10:00:00'", 'calendar_origin':"'2024-02-01 10:00:00'"} ) diff --git a/components/lfric-xios/integration-test/lfric_xios_time_read_test.f90 b/components/lfric-xios/integration-test/lfric_xios_time_read_test.f90 index 0e500544a..c26d1c577 100755 --- a/components/lfric-xios/integration-test/lfric_xios_time_read_test.f90 +++ b/components/lfric-xios/integration-test/lfric_xios_time_read_test.f90 @@ -47,7 +47,7 @@ program lfric_xios_time_read_test xios_date(2024, 1, 1, 15, 8, 0), & xios_date(2024, 1, 1, 15, 9, 0), & xios_date(2024, 1, 1, 15, 10, 0) ] - result = read_time_data("lfric_xios_temporal_input") + result = read_time_data("lfric_xios_time_read_data") do t = 1, size(result) if (result(t) /= check(t)) then diff --git a/components/lfric-xios/integration-test/lfric_xios_time_read_test.py b/components/lfric-xios/integration-test/lfric_xios_time_read_test.py index 93f2fea64..3c463a9a2 100755 --- a/components/lfric-xios/integration-test/lfric_xios_time_read_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_time_read_test.py @@ -22,9 +22,9 @@ class LfricXiosTimeReadTest(LFRicXiosTest): # pylint: disable=too-few-public-me def __init__(self, nprocs: int): super().__init__(command=[sys.argv[1], "resources/configs/context.nml"], processes=nprocs) - test_data_dir = Path(Path.cwd(), 'resources/data') - Path('lfric_xios_time_read_data.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path('lfric_xios_time_read_data.nc')) + test_data_dir = Path(self.resources_dir, 'data') + Path(self.test_working_dir, 'lfric_xios_time_read_data.nc').unlink(missing_ok=True) + self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_time_read_data.nc')) self.nprocs = nprocs def test(self, returncode: int, out: str, err: str): From d305e9983150c5158bcaff1e74081599d61d54fa Mon Sep 17 00:00:00 2001 From: EdHone Date: Mon, 12 Jan 2026 10:12:23 +0000 Subject: [PATCH 7/7] Testing framework fully fixed --- .../build/testframework/xiostest.py | 48 +++++++++------- .../lfric_xios_context_test.py | 3 +- .../lfric_xios_temporal_iodef_test.py | 37 +++++-------- .../lfric_xios_temporal_test.py | 55 ++++++------------- .../lfric_xios_time_read_test.py | 7 +-- 5 files changed, 65 insertions(+), 85 deletions(-) diff --git a/components/lfric-xios/build/testframework/xiostest.py b/components/lfric-xios/build/testframework/xiostest.py index 07ee69909..2d7fd020d 100644 --- a/components/lfric-xios/build/testframework/xiostest.py +++ b/components/lfric-xios/build/testframework/xiostest.py @@ -27,6 +27,8 @@ def __init__(self, command=sys.argv[1], processes=1, iodef_file=None): else: self.iodef_file = iodef_file + super().__init__(command, processes) + self.xios_out: List[XiosOutput] = [] self.xios_err: List[XiosOutput] = [] @@ -34,47 +36,53 @@ def __init__(self, command=sys.argv[1], processes=1, iodef_file=None): self.test_top_level: Path = Path(os.getcwd()) self.resources_dir: Path = self.test_top_level / "resources" self.test_working_dir: Path = self.test_top_level / "working" / type(self).__name__ - - # Replace resource path with absolute path in command - for i in range(len(command)): - if "resources/" in command[i]: - command[i] = command[i].replace("resources/", str(self.resources_dir) + "/") - - super().__init__(command, processes) - if not os.path.exists(self.test_working_dir): os.makedirs(self.test_working_dir) + # Create symlink to test executable in working directory + if not os.path.exists(Path(self.test_working_dir) / command[0].split("/")[-1]): + os.symlink(Path(command[0]), Path(self.test_working_dir) / command[0].split("/")[-1]) + # Change to test working directory os.chdir(self.test_working_dir) - def gen_data(self, source: Path, dest: Path): + def gen_data(self, source: str, dest: str): """ - Create input data files from CDL formatted text. + Create input data files from CDL formatted text. Looks for source file + in resources/data directory and generates dest file in test working directory. """ + dest_path: Path = Path(self.test_working_dir) / Path(dest) + source_path: Path = Path(self.resources_dir, 'data') / Path(source) + dest_path.unlink(missing_ok=True) + proc = subprocess.Popen( - ['ncgen', '-k', 'nc4', '-o', f'{dest}', f'{source}'], + ['ncgen', '-k', 'nc4', '-o', f'{dest_path}', f'{source_path }'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) _, err = proc.communicate() if proc.returncode != 0: raise Exception("Test data generation failed:\n" + f"{err}") - - def gen_config(self, config_source: Path, config_out: Path, new_config: dict): + + + def gen_config(self, config_source: str, config_out: str, new_config: dict): """ - Create an LFRic configuration namelist. + Create an LFRic configuration namelist. Looks for source file + in resources/configs directory and generates dest file in test working directory. """ - config_in = open(config_source, 'r') + config_in = open(Path(self.resources_dir, 'configs', config_source), 'r') config = config_in.readlines() for key in new_config.keys(): for i in range(len(config)): if key in config[i]: - config[i] = f" {key}={new_config[key]}\n" + if type(new_config[key]) == str: + config[i] = f" {key}='{new_config[key]}'\n" + else: + config[i] = f" {key}={new_config[key]}\n" config_in.close() - f = open(config_out, "w") + f = open(Path(self.test_working_dir, config_out), "w") for line in config: f.write(line) f.close() @@ -130,8 +138,8 @@ def post_execution(self, return_code): """ for proc in range(self._processes): - self.xios_out.append(XiosOutput(f"xios_client_{proc}.out")) - self.xios_err.append(XiosOutput(f"xios_client_{proc}.err")) + self.xios_out.append(XiosOutput(self.test_working_dir / f"xios_client_{proc}.out")) + self.xios_err.append(XiosOutput(self.test_working_dir / f"xios_client_{proc}.err")) # Return to top level directory os.chdir(self.test_top_level) @@ -143,7 +151,7 @@ class XiosOutput: """ def __init__(self, filename): - self.path: Path = Path(os.getcwd()) / Path(filename) + self.path: Path = Path(filename) with open(self.path, "rt") as handle: self.contents = handle.read() diff --git a/components/lfric-xios/integration-test/lfric_xios_context_test.py b/components/lfric-xios/integration-test/lfric_xios_context_test.py index 7d22de1f2..341138f26 100755 --- a/components/lfric-xios/integration-test/lfric_xios_context_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_context_test.py @@ -21,7 +21,8 @@ class LfricXiosContextTest(LFRicXiosTest): """ def __init__(self): - super().__init__(command=[sys.argv[1], "resources/configs/context.nml"], processes=1) + super().__init__(command=[sys.argv[1], "context.nml"], processes=1) + self.gen_config( "context.nml", "context.nml", {} ) def test(self, returncode: int, out: str, err: str): """ diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py b/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py index 828a3bfd8..89801a0fb 100755 --- a/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_iodef_test.py @@ -24,12 +24,9 @@ class LfricXiosFullNonCyclicIodefTest(LFRicXiosTest): # pylint: disable=too-few """ def __init__(self): - super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1, iodef_file="iodef_temporal.xml") - test_data_dir = Path(self.resources_dir, 'data') - Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) - self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), - Path(self.resources_dir, "configs/non_cyclic_full.nml"), {} ) + super().__init__(command=[sys.argv[1], "non_cyclic_full.nml"], processes=1, iodef_file="iodef_temporal.xml") + self.gen_data('temporal_data.cdl', 'lfric_xios_temporal_input.nc') + self.gen_config( "non_cyclic_base.nml", "non_cyclic_full.nml", {} ) def test(self, returncode: int, out: str, err: str): """ @@ -38,7 +35,7 @@ def test(self, returncode: int, out: str, err: str): if returncode != 0: print(out) - raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + + raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + f"stderr:\n" + f"{err}") if not self.nc_data_match(Path(self.test_working_dir, 'lfric_xios_temporal_input.nc'), @@ -56,14 +53,10 @@ class LfricXiosFullNonCyclicIodefHighFreqTest(LFRicXiosTest): # pylint: disable """ def __init__(self): - super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1, iodef_file="iodef_temporal.xml") - test_data_dir = Path(self.resources_dir, 'data') - Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) - self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), - Path(self.resources_dir, "configs/non_cyclic_full.nml"), - {"dt": 10.0, - "timestep_end": 60} ) + super().__init__(command=[sys.argv[1], "non_cyclic_full.nml"], processes=1, iodef_file="iodef_temporal.xml") + self.gen_data('temporal_data.cdl', 'lfric_xios_temporal_input.nc') + self.gen_config( "non_cyclic_base.nml", "non_cyclic_full.nml", {"dt": 10.0, + "timestep_end": '60'} ) def test(self, returncode: int, out: str, err: str): """ @@ -90,22 +83,20 @@ class LfricXiosFullNonCyclicIodefNoFreqTest(LFRicXiosTest): # pylint: disable=t """ def __init__(self): - super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) - test_data_dir = Path(self.resources_dir, 'data') - Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) - self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), - Path(self.resources_dir, "configs/non_cyclic_full.nml"), {} ) + super().__init__(command=[sys.argv[1], "non_cyclic_full.nml"], processes=1) + self.gen_data('temporal_data.cdl', 'lfric_xios_temporal_input.nc') + self.gen_config( "non_cyclic_base.nml", "non_cyclic_full.nml", {} ) def test(self, returncode: int, out: str, err: str): """ Test the output of the context test """ - expected_xios_err = 'In file "type_impl.hpp", function "void xios::CType::_checkEmpty() const [with T = xios::CDuration]", line 210 -> Data is not initialized' + expected_xios_errs = ['In file "type_impl.hpp", function "void xios::CType::_checkEmpty() const [with T = xios::CDuration]", line 210 -> Data is not initialized', + 'In file "type_impl.hpp", function "void xios::CType::_checkEmpty() const [T = xios::CDuration]", line 210 -> Data is not initialized'] if returncode == 134: - if self.xios_err[0].contents.strip() == expected_xios_err: + if self.xios_err[0].contents.strip() in expected_xios_errs: return "Expected failure of test executable due to missing frequency setting." else: raise TestFailed("Test executable failed, but with unexpected error message.") diff --git a/components/lfric-xios/integration-test/lfric_xios_temporal_test.py b/components/lfric-xios/integration-test/lfric_xios_temporal_test.py index 1e96f4740..361913de4 100755 --- a/components/lfric-xios/integration-test/lfric_xios_temporal_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_temporal_test.py @@ -23,12 +23,9 @@ class LfricXiosFullNonCyclicTest(LFRicXiosTest): # pylint: disable=too-few-publ """ def __init__(self): - super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_full.nml"], processes=1) - test_data_dir = Path(self.resources_dir, 'data') - Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) - self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), - Path(self.resources_dir, "configs/non_cyclic_full.nml"), {} ) + super().__init__(command=[sys.argv[1], "non_cyclic_full.nml"], processes=1) + self.gen_data('temporal_data.cdl', 'lfric_xios_temporal_input.nc') + self.gen_config( "non_cyclic_base.nml", "non_cyclic_full.nml", {} ) def test(self, returncode: int, out: str, err: str): """ @@ -37,7 +34,7 @@ def test(self, returncode: int, out: str, err: str): if returncode != 0: print(out) - raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + + raise TestFailed(f"Unexpected failure of test executable: {returncode}\n" + f"stderr:\n" + f"{err}") if not self.nc_data_match(Path(self.test_working_dir, 'lfric_xios_temporal_input.nc'), @@ -55,13 +52,9 @@ class LfricXiosNonCyclicHighFreqTest(LFRicXiosTest): # pylint: disable=too-few- """ def __init__(self): - super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_high_freq.nml"], processes=1) - test_data_dir = Path(self.resources_dir, 'data') - Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) - self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), - Path(self.resources_dir, "configs/non_cyclic_high_freq.nml"), - {"dt":"10.0"} ) + super().__init__(command=[sys.argv[1], "non_cyclic_high_freq.nml"], processes=1) + self.gen_data('temporal_data.cdl', 'lfric_xios_temporal_input.nc') + self.gen_config( "non_cyclic_base.nml", "non_cyclic_high_freq.nml", {"dt":10.0} ) def test(self, returncode: int, out: str, err: str): """ @@ -88,13 +81,9 @@ class LfricXiosPartialNonCyclicTest(LFRicXiosTest): # pylint: disable=too-few-p """ def __init__(self): - super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_mid.nml"], processes=1) - test_data_dir = Path(self.resources_dir, 'data') - Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) - self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), - Path(self.resources_dir, "configs/non_cyclic_mid.nml"), - {'calendar_start':"'2024-01-01 15:01:00'"} ) + super().__init__(command=[sys.argv[1], "non_cyclic_mid.nml"], processes=1) + self.gen_data('temporal_data.cdl', 'lfric_xios_temporal_input.nc') + self.gen_config( "non_cyclic_base.nml", "non_cyclic_mid.nml", {'calendar_start':'2024-01-01 15:01:00'} ) def test(self, returncode: int, out: str, err: str): """ @@ -120,14 +109,10 @@ class LfricXiosNonCyclicFutureTest(LFRicXiosTest): # pylint: disable=too-few-pu """ def __init__(self): - super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_future.nml"], processes=1) - test_data_dir = Path(self.resources_dir, 'data') - Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) - self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), - Path(self.resources_dir, "configs/non_cyclic_future.nml"), - {'calendar_start':"'2024-01-01 10:00:00'", - 'calendar_origin':"'2024-01-01 10:00:00'"} ) + super().__init__(command=[sys.argv[1], "non_cyclic_future.nml"], processes=1) + self.gen_data('temporal_data.cdl', 'lfric_xios_temporal_input.nc') + self.gen_config( "non_cyclic_base.nml", "non_cyclic_future.nml", {'calendar_start':'2024-01-01 10:00:00', + 'calendar_origin':'2024-01-01 10:00:00'} ) def test(self, returncode: int, out: str, err: str): """ @@ -153,14 +138,10 @@ class LfricXiosNonCyclicPastTest(LFRicXiosTest): # pylint: disable=too-few-publ """ def __init__(self): - super().__init__(command=[sys.argv[1], "resources/configs/non_cyclic_past.nml"], processes=1) - test_data_dir = Path(self.resources_dir, 'data') - Path(self.test_working_dir, 'lfric_xios_temporal_input.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_temporal_input.nc')) - self.gen_config( Path(self.resources_dir, "configs/non_cyclic_base.nml"), - Path(self.resources_dir, "configs/non_cyclic_past.nml"), - {'calendar_start':"'2024-02-01 10:00:00'", - 'calendar_origin':"'2024-02-01 10:00:00'"} ) + super().__init__(command=[sys.argv[1], "non_cyclic_past.nml"], processes=1) + self.gen_data('temporal_data.cdl', 'lfric_xios_temporal_input.nc') + self.gen_config( "non_cyclic_base.nml", "non_cyclic_past.nml", {'calendar_start':'2024-02-01 10:00:00', + 'calendar_origin':'2024-02-01 10:00:00'} ) def test(self, returncode: int, out: str, err: str): """ diff --git a/components/lfric-xios/integration-test/lfric_xios_time_read_test.py b/components/lfric-xios/integration-test/lfric_xios_time_read_test.py index 3c463a9a2..a20a90b73 100755 --- a/components/lfric-xios/integration-test/lfric_xios_time_read_test.py +++ b/components/lfric-xios/integration-test/lfric_xios_time_read_test.py @@ -21,10 +21,9 @@ class LfricXiosTimeReadTest(LFRicXiosTest): # pylint: disable=too-few-public-me """ def __init__(self, nprocs: int): - super().__init__(command=[sys.argv[1], "resources/configs/context.nml"], processes=nprocs) - test_data_dir = Path(self.resources_dir, 'data') - Path(self.test_working_dir, 'lfric_xios_time_read_data.nc').unlink(missing_ok=True) - self.gen_data(Path(test_data_dir, 'temporal_data.cdl'), Path(self.test_working_dir, 'lfric_xios_time_read_data.nc')) + super().__init__(command=[sys.argv[1], "context.nml"], processes=nprocs) + self.gen_data('temporal_data.cdl', 'lfric_xios_time_read_data.nc') + self.gen_config( "context.nml", "context.nml", {} ) self.nprocs = nprocs def test(self, returncode: int, out: str, err: str):