Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
57b1807
Add external tutorial sources support (git/archive) for systemtests
PranjalManhgaye Mar 1, 2026
2a409bb
Merge branch 'develop' into external-source-fix
PranjalManhgaye May 18, 2026
4d68a31
Fix pre-commit style checks for PR #732
PranjalManhgaye May 18, 2026
55bb04b
Merge branch 'develop' into external-source-fix
PranjalManhgaye Jun 16, 2026
ee526d4
Merge branch 'develop' into external-source-fix
MakisH Jun 24, 2026
a9e6c78
Add a concrete example in the tests.yaml
MakisH Jun 24, 2026
8424e57
Refine external test config shape per review on PR #732
PranjalManhgaye Jun 24, 2026
88d16d0
Merge branch 'develop' into external-source-fix
PranjalManhgaye Jun 30, 2026
567662a
Merge branch 'develop' into external-source-fix
PranjalManhgaye Jul 1, 2026
4b30bce
Revert change in Quickstart
MakisH Jul 1, 2026
026816c
Rearrange test suites
MakisH Jul 1, 2026
87803f6
Convert a test case from git to zip
MakisH Jul 1, 2026
a07e6cb
Fix type of archive
MakisH Jul 1, 2026
bd711a6
Remove external from release
MakisH Jul 1, 2026
c9b1b6e
Set a longer timeous
MakisH Jul 1, 2026
1a46abd
Set longer timeous
MakisH Jul 1, 2026
a81795e
Split test suite
MakisH Jul 1, 2026
0b29acb
Improve documentation
MakisH Jul 1, 2026
b1f07e4
Address Jul 1 review on external test sources
PranjalManhgaye Jul 1, 2026
47dfcf7
Clarify shell-script executable-bit helper
PranjalManhgaye Jul 3, 2026
10b3fba
Update/reorder list of tests
MakisH Jul 3, 2026
e3b0b28
Merge branch 'develop' into external-source-fix
MakisH Jul 3, 2026
8b55734
Log external fetch/cache and prefix External: in case names
PranjalManhgaye Jul 3, 2026
8bdabf6
Merge branch 'develop' into external-source-fix
MakisH Jul 3, 2026
32e81c5
Resolve external tutorials once at parse time
PranjalManhgaye Jul 3, 2026
865edb6
Apply suggestion from @MakisH
MakisH Jul 3, 2026
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/732.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added optional `source` blocks in `tests.yaml` so system tests can load tutorials from external git repositories or archives ([#732](https://github.com/precice/tutorials/pull/732)).
36 changes: 35 additions & 1 deletion tools/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,40 @@ Note that you will need to define the `TUTORIALS_REF` in the file [`reference_ve

The results will be added to a Git LFS, but you will need special push access: just use the aforementioned GitHub Actions workflow, instead.

#### External test sources

Local test cases are defined in the `tutorials:` list. For test cases maintained in other git repositories or available as archives, use the `external:` list. A test suite may define both lists so that external cases can be referenced via YAML anchors alongside local tutorials (for example in the `release` suite).

```yaml
test_suites:
some-external-test-case:
external:
- &some-external-test-case
source:
type: git
url: https://github.com/some-user/tutorials.git
ref: some-branch
subdir: .
path: path-to-test-case
case_combination:
...

release:
tutorials:
- *quickstart_openfoam_cpp
external:
- *some-external-test-case
```

Supported `source.type` values:

- `git`: shallow-clone `url` at `ref`. The `ref` must exist on the remote; clone failures are reported as errors.
- `archive`: download and extract a `.tar.gz` / `.zip` from `url`.

Fetched external sources are cached under `~/.cache/precice-tutorials` (or `PRECICE_EXTERNAL_CACHE_DIR`) so that repeated test runs and CI matrix jobs do not re-download the same repository or archive every time. The cache key is derived from the source URL, ref, and optional subdir.

The runner fetches the tutorial, copies it into the run directory, and then continues with the usual Docker build/run and fieldcompare steps. `TUTORIALS_REF` / `TUTORIALS_PR` build arguments apply only to test cases sourced from the tutorials repository; external test cases are pinned by `source.ref`. Reference result paths are resolved relative to the root directory of the fetched test case.

### Adding new components

To add a new component, a few changes are needed:
Expand Down Expand Up @@ -299,7 +333,7 @@ cases:
Description:

- `name`: A human-readable, descriptive name
- `path`: Where the tutorial is located, relative to the tutorials repository
- `path`: Where the tutorial is located, relative to the tutorials repository (or the tutorial folder name for external sources)
- `url`: A web page with more information on the tutorial
- `participants`: A list of preCICE participants, typically corresponding to different domains of the simulation
- `cases`: A list of solver configuration directories. Each element of the list includes:
Expand Down
25 changes: 20 additions & 5 deletions tools/tests/metadata_parser/metdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import itertools
from paths import PRECICE_TESTS_DIR, PRECICE_TUTORIAL_DIR

from systemtests.sources import TutorialSource


@dataclass
class BuildArgument:
Expand Down Expand Up @@ -283,13 +285,15 @@ def from_cases_tuple(cls, cases: Tuple[Case], tutorial: Tutorial):
class ReferenceResult:
path: Path
case_combination: CaseCombination
base_dir: Path | None = None

def __repr__(self) -> str:
return f"{self.path.as_posix()}"

def __post_init__(self):
# built full path
self.path = PRECICE_TUTORIAL_DIR / self.path
base = self.base_dir if self.base_dir is not None else PRECICE_TUTORIAL_DIR
self.path = Path(base) / self.path


@dataclass
Expand All @@ -303,6 +307,10 @@ class Tutorial:
url: str
participants: List[str]
cases: List[Case]
source: "TutorialSource" = field(default_factory=TutorialSource.local)
# Filesystem path to the fetched external tutorial, resolved once at parse
# time and reused when copying into the run directory (None for local).
resolved_root: "Path | None" = None
case_combinations: List[CaseCombination] = field(init=False)

def __post_init__(self):
Expand Down Expand Up @@ -359,29 +367,36 @@ def get_case_by_string(self, case_name: str) -> Optional[Case]:
return None

@classmethod
def from_yaml(cls, path, available_components):
def from_yaml(cls, path, available_components, base_dir=None, source=None):
"""
Creates a Tutorial instance from a YAML file.

Args:
path: The path to the YAML file.
path: The path to the metadata.yaml file.
available_components: The Components instance containing available components.
base_dir: Optional base directory for resolving tutorial path (for external sources).
Defaults to PRECICE_TUTORIAL_DIR.
source: Optional TutorialSource (for external tutorials).

Returns:
An instance of Tutorial.
"""
with open(path, 'r') as f:
data = yaml.safe_load(f)
name = data['name']
path = PRECICE_TUTORIAL_DIR / data['path']
base = base_dir if base_dir is not None else PRECICE_TUTORIAL_DIR
tutorial_path = Path(base) / data['path']
url = data['url']
participants = data.get('participants', [])
cases_raw = data.get('cases', {})
cases = []
for case_name in cases_raw.keys():
cases.append(Case.from_dict(
case_name, cases_raw[case_name], available_components))
return cls(name, path, url, participants, cases)
tut = cls(name, tutorial_path, url, participants, cases)
if source is not None:
tut.source = source
return tut


class Tutorials(list):
Expand Down
44 changes: 29 additions & 15 deletions tools/tests/systemtests/Systemtest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import hashlib
import subprocess
import threading
from .sources import resolve_tutorial_root, PRECICE_EXTERNAL_CACHE_DIR
from typing import List, Dict, Optional, Tuple
from jinja2 import Environment, FileSystemLoader
from dataclasses import dataclass, field
Expand Down Expand Up @@ -480,24 +481,36 @@ def __copy_tutorial_into_directory(self, run_directory: Path):
"""
current_time_string = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.run_directory = run_directory
pr_requested = self.params_to_use.get("TUTORIALS_PR")
if pr_requested:
logging.debug(f"Fetching the PR {pr_requested} HEAD reference")
self._fetch_pr(PRECICE_TUTORIAL_DIR, pr_requested)
current_ref = self._get_git_ref(PRECICE_TUTORIAL_DIR)
ref_requested = self.params_to_use.get("TUTORIALS_REF")
if ref_requested:
logging.debug(f"Checking out tutorials {ref_requested} before copying")
self._fetch_ref(PRECICE_TUTORIAL_DIR, ref_requested)
self._checkout_ref_in_subfolder(PRECICE_TUTORIAL_DIR, self.tutorial.path, ref_requested)

self.tutorial_folder = slugify(f'{self.tutorial.path.name}_{self.case_combination.cases}_{current_time_string}')
current_ref = None
ref_requested = None

if self.tutorial.source.type == "local":
pr_requested = self.params_to_use.get("TUTORIALS_PR")
if pr_requested:
logging.debug(f"Fetching the PR {pr_requested} HEAD reference")
self._fetch_pr(PRECICE_TUTORIAL_DIR, pr_requested)
current_ref = self._get_git_ref(PRECICE_TUTORIAL_DIR)
ref_requested = self.params_to_use.get("TUTORIALS_REF")
if ref_requested:
logging.debug(f"Checking out tutorials {ref_requested} before copying")
self._fetch_ref(PRECICE_TUTORIAL_DIR, ref_requested)
self._checkout_ref_in_subfolder(
PRECICE_TUTORIAL_DIR, self.tutorial.path, ref_requested)

self.tutorial_folder = slugify(
f'{self.tutorial.path.name}_{self.case_combination.cases}_{current_time_string}')
destination = run_directory / self.tutorial_folder
src = self.tutorial.path
# External sources are fetched and resolved once at parse time; reuse
# that path here to avoid a redundant fetch (and duplicate log line).
src = self.tutorial.resolved_root or resolve_tutorial_root(
self.tutorial.path,
self.tutorial.source,
PRECICE_EXTERNAL_CACHE_DIR,
)
self.system_test_dir = destination
shutil.copytree(src, destination)

if ref_requested:
if self.tutorial.source.type == "local" and ref_requested:
with open(destination / "tutorials_ref", 'w') as file:
file.write(ref_requested)
self._checkout_ref_in_subfolder(PRECICE_TUTORIAL_DIR, self.tutorial.path, current_ref)
Expand Down Expand Up @@ -971,7 +984,8 @@ def _run_tutorial(self):
return DockerComposeResult(exit_code, stdout_data, stderr_data, self, elapsed_time)

def __repr__(self):
return f"{self.tutorial.name} {self.case_combination}"
prefix = "External: " if getattr(self.tutorial.source, "type", "local") != "local" else ""
return f"{prefix}{self.tutorial.name} {self.case_combination}"

def __apply_max_time_override(self):
"""Overwrite <max-time> or <max-time-windows> value in precice-config.xml."""
Expand Down
Loading