Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog-entries/402.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add per-test `max_time` override in `tests.yaml` to cap preCICE simulation time without editing `precice-config.xml` manually. Applies consistently to both test runs and reference result generation. Handles multiple `<max-time>` tags with a warning.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handles multiple <max-time> tags with a warning.

This should be a warning in preCICE library itself, not in the system tests.

3 changes: 3 additions & 0 deletions tools/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ In order for the systemtests to pick up the tutorial we need to define a `metada
To add a testsuite just open the `tests.yaml` file and use the output of `python print_case_combinations.py` to add the right case combinations you want to test. Note that you can specify a `reference_result` which is not yet present. The `generate_reference_data.py` will pick that up and create it for you.
Note that its important to carefully check the paths of the `reference_result` in order to not have typos in there. Also note that same cases in different testsuites should use the same `reference_result`.

To cap the simulation time without editing `precice-config.xml` manually, add an optional `max_time` field (positive number, in seconds) to any tutorial entry. This overrides the `<max-time>` tag in `precice-config.xml` at run time, and applies consistently to both test runs and reference result generation.

### Generate reference results

Since we need data to compare against, you need to run `python generate_reference_data.py`. This process might take a while.
Expand Down Expand Up @@ -319,6 +321,7 @@ test_suites:
- fluid-openfoam
- solid-openfoam
reference_result: ./flow-over-heated-plate/reference-results/fluid-openfoam_solid-openfoam.tar.gz
max_time: 10.0 # optional: overrides <max-time> in precice-config.xml (seconds)
openfoam_adapter_release:
tutorials:
- path: flow-over-heated-plate
Expand Down
10 changes: 5 additions & 5 deletions tools/tests/components.yaml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, see #731 and #735.

Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ python-bindings:
description: Tutorial git reference to use
default: "master"
PYTHON_BINDINGS_REF:
semnantic: Git ref of the Python bindings to use
description: Git ref of the Python bindings to use
default: "master"

openfoam-adapter:
Expand Down Expand Up @@ -75,10 +75,10 @@ fenics-adapter:
description: Tutorial git reference to use
default: "master"
PYTHON_BINDINGS_REF:
semnantic: Git ref of the Python bindings to use
description: Git ref of the Python bindings to use
default: "master"
FENICS_ADAPTER_REF:
semnantic: Git ref of the fenics adapter to use
description: Git ref of the fenics adapter to use
default: "master"

nutils-adapter:
Expand All @@ -98,7 +98,7 @@ nutils-adapter:
description: Tutorial git reference to use
default: "master"
PYTHON_BINDINGS_REF:
semnantic: Git ref of the Python bindings to use
description: Git ref of the Python bindings to use
default: "master"

calculix-adapter:
Expand Down Expand Up @@ -190,7 +190,7 @@ dumux-adapter:
description: Version of DuMux to use
default: "3.7"
DUMUX_ADAPTER_REF:
semnantic: Git ref of the dumux adapter to use
description: Git ref of the dumux adapter to use
default: "main"

micro-manager:
Expand Down
7 changes: 4 additions & 3 deletions tools/tests/generate_reference_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,11 @@ def main():
for test_suite in test_suites:
tutorials = test_suite.cases_of_tutorial.keys()
for tutorial in tutorials:
for case, reference_result in zip(
test_suite.cases_of_tutorial[tutorial], test_suite.reference_results[tutorial]):
max_times = test_suite.max_times.get(tutorial, [None] * len(test_suite.cases_of_tutorial[tutorial]))
for case, reference_result, max_time in zip(
test_suite.cases_of_tutorial[tutorial], test_suite.reference_results[tutorial], max_times):
systemtests_to_run.add(
Systemtest(tutorial, build_args, case, reference_result))
Systemtest(tutorial, build_args, case, reference_result, max_time=max_time))

reference_result_per_tutorial = {}
current_time_string = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
Expand Down
7 changes: 4 additions & 3 deletions tools/tests/systemtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ def main():
for test_suite in test_suites_to_execute:
tutorials = test_suite.cases_of_tutorial.keys()
for tutorial in tutorials:
for case, reference_result in zip(
test_suite.cases_of_tutorial[tutorial], test_suite.reference_results[tutorial]):
max_times = test_suite.max_times.get(tutorial, [None] * len(test_suite.cases_of_tutorial[tutorial]))
for case, reference_result, max_time in zip(
test_suite.cases_of_tutorial[tutorial], test_suite.reference_results[tutorial], max_times):
systemtests_to_run.append(
Systemtest(tutorial, build_args, case, reference_result))
Systemtest(tutorial, build_args, case, reference_result, max_time=max_time))

if not systemtests_to_run:
raise RuntimeError("Did not find any Systemtests to execute.")
Expand Down
50 changes: 46 additions & 4 deletions tools/tests/systemtests/Systemtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import os


GLOBAL_TIMEOUT = 900
BUILD_TIMEOUT = 900
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated change, comes from #731 / #735. Please revert, to keep the PRs independently mergeable.

SHORT_TIMEOUT = 10


Expand Down Expand Up @@ -134,6 +134,7 @@ class Systemtest:
arguments: SystemtestArguments
case_combination: CaseCombination
reference_result: ReferenceResult
max_time: Optional[float] = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optionl seems to be an old practice: https://stackoverflow.com/a/51710151/2254346

But I am not very up-to-date in Python.

params_to_use: Dict[str, str] = field(init=False)
env: Dict[str, str] = field(init=False)

Expand Down Expand Up @@ -394,7 +395,7 @@ def _run_field_compare(self):
cwd=self.system_test_dir)

try:
stdout, stderr = process.communicate(timeout=GLOBAL_TIMEOUT)
stdout, stderr = process.communicate(timeout=self.timeout)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originates from #735, please separate the two PRs.

except KeyboardInterrupt as k:
process.kill()
raise KeyboardInterrupt from k
Expand Down Expand Up @@ -439,7 +440,7 @@ def _build_docker(self):
cwd=self.system_test_dir)

try:
stdout, stderr = process.communicate(timeout=GLOBAL_TIMEOUT)
stdout, stderr = process.communicate(timeout=BUILD_TIMEOUT)
except KeyboardInterrupt as k:
process.kill()
# process.send_signal(9)
Expand Down Expand Up @@ -483,7 +484,7 @@ def _run_tutorial(self):
cwd=self.system_test_dir)

try:
stdout, stderr = process.communicate(timeout=GLOBAL_TIMEOUT)
stdout, stderr = process.communicate(timeout=self.timeout)
except KeyboardInterrupt as k:
process.kill()
# process.send_signal(9)
Expand Down Expand Up @@ -513,11 +514,52 @@ def __write_logs(self, stdout_data: List[str], stderr_data: List[str]):
with open(self.system_test_dir / "stderr.log", 'w') as stderr_file:
stderr_file.write("\n".join(stderr_data))

def __apply_precice_max_time_override(self):
"""
If max_time is set, override the <max-time value="..."/> in precice-config.xml
of the copied tutorial directory. Applies to both test runs and reference generation.
Targets only <max-time> tags to avoid modifying time-window-size or other attributes.
"""
if self.max_time is None:
return
if not (isinstance(self.max_time, (int, float)) and self.max_time > 0):
logging.warning(
f"Invalid max_time {self.max_time!r} for {self}; must be a positive number. Skipping override.")
return
config_path = self.system_test_dir / "precice-config.xml"
if not config_path.exists():
logging.warning(
f"Requested max_time override for {self}, but no precice-config.xml "
f"found in {self.system_test_dir}")
return
Comment on lines +529 to +534
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should never happen in the system tests: all tests have a precice-config.xml, by definition. I would remove it to keep the code simple.

try:
text = config_path.read_text()
except Exception as e:
logging.warning(f"Could not read {config_path} to apply max_time override: {e}")
return
pattern = r'(<max-time\s+value=")([^"]*)(")'
matches = re.findall(pattern, text)
if not matches:
logging.warning(
f"Requested max_time override for {self}, but no <max-time .../> tag "
f"found in {config_path}")
return
if len(matches) > 1:
logging.warning(
f"Multiple <max-time> tags found in {config_path}; overriding all to {self.max_time}")
new_text = re.sub(pattern, rf"\g<1>{self.max_time}\g<3>", text)
try:
config_path.write_text(new_text)
logging.info(f"Overwrote max-time in {config_path} to {self.max_time} for {self}")
except Exception as e:
logging.warning(f"Failed to write updated {config_path}: {e}")

Comment on lines +535 to +556
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that max-time is not the only way to restrict the time. There is also max-time-windows.

https://precice.org/configuration-xml-reference.html#max-time

https://precice.org/configuration-xml-reference.html#max-time-windows

I would leave it up to the user to set the right thing, and not add so much error handling. I think this function is a bit more complicated than it needs to be.

Note that an alternative approach would be to extend the docker compose service before running. We already set some of these in the template, e.g., to always export VTK files:

https://github.com/precice/tutorials/blob/develop/tools/tests/docker-compose.template.yaml

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, for shorter tests, it would be convenient to directly define max-time-windows, instead of max-time, because then we don't need to look at the time-window-size to decide on the value.

As a maintainer, I would be interested to know that, e.g., three coupling time windows complete, since most problems appear there. The time is more relevant for the application, not for the coupling.

def __prepare_for_run(self, run_directory: Path):
"""
Prepares the run_directory with folders and datastructures needed for every systemtest execution
"""
self.__copy_tutorial_into_directory(run_directory)
self.__apply_precice_max_time_override()
self.__copy_tools(run_directory)
self.__put_gitignore(run_directory)
host_uid, host_gid = self.__get_uid_gid()
Expand Down
12 changes: 11 additions & 1 deletion tools/tests/systemtests/TestSuite.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class TestSuite:
name: str
cases_of_tutorial: Dict[Tutorial, List[CaseCombination]]
reference_results: Dict[Tutorial, List[ReferenceResult]]
max_times: Dict[Tutorial, List[Optional[float]]] = field(default_factory=dict)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly to #735: Why is this a (dictionary of a) list per tutorial, and not a value per CaseCombination?


def __repr__(self) -> str:
return_string = f"Test suite: {self.name} contains:"
Expand Down Expand Up @@ -48,6 +49,7 @@ def from_yaml(cls, path, parsed_tutorials: Tutorials):
for test_suite_name in test_suites_raw:
case_combinations_of_tutorial = {}
reference_results_of_tutorial = {}
max_times_of_tutorial = {}
# iterate over tutorials:
for tutorial_case in test_suites_raw[test_suite_name]['tutorials']:
tutorial = parsed_tutorials.get_by_path(tutorial_case['path'])
Expand All @@ -57,6 +59,7 @@ def from_yaml(cls, path, parsed_tutorials: Tutorials):
if tutorial not in case_combinations_of_tutorial:
case_combinations_of_tutorial[tutorial] = []
reference_results_of_tutorial[tutorial] = []
max_times_of_tutorial[tutorial] = []

all_case_combinations = tutorial.case_combinations
case_combination_requested = CaseCombination.from_string_list(
Expand All @@ -65,12 +68,19 @@ def from_yaml(cls, path, parsed_tutorials: Tutorials):
case_combinations_of_tutorial[tutorial].append(case_combination_requested)
reference_results_of_tutorial[tutorial].append(ReferenceResult(
tutorial_case['reference_result'], case_combination_requested))
raw_max_time = tutorial_case.get('max_time')
if raw_max_time is not None:
if not (isinstance(raw_max_time, (int, float)) and raw_max_time > 0):
raise ValueError(
f"Invalid max_time {raw_max_time!r} for tutorial "
f"'{tutorial_case['path']}'; must be a positive number.")
max_times_of_tutorial[tutorial].append(raw_max_time)
else:
raise Exception(
f"Could not find the following cases {tutorial_case['case-combination']} in the current metadata of tutorial {tutorial.name}")

testsuites.append(TestSuite(test_suite_name, case_combinations_of_tutorial,
reference_results_of_tutorial))
reference_results_of_tutorial, max_times_of_tutorial))

return cls(testsuites)

Expand Down