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/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)).
38 changes: 37 additions & 1 deletion tools/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,42 @@ 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_left-openfoam_right-openfoam
source:
type: git
url: https://github.com/some-user/tutorials.git
ref: some-branch
subdir: .
path: path-to-test-case
case_combination:
- left-openfoam
- right-openfoam
reference_result: ./path-to-test-case/reference_results/participant-combination.tar.gz

release:
tutorials:
- *quickstart_openfoam_cpp
external:
- *some-external-test-case_left-openfoam_right-openfoam
```

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 +335,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
22 changes: 17 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,7 @@ class Tutorial:
url: str
participants: List[str]
cases: List[Case]
source: "TutorialSource" = field(default_factory=TutorialSource.local)
case_combinations: List[CaseCombination] = field(init=False)

def __post_init__(self):
Expand Down Expand Up @@ -359,29 +364,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
39 changes: 25 additions & 14 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,34 @@ 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
src = 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
Loading