Skip to content

Commit 3ba6044

Browse files
Add external tutorial sources support (git/archive) for systemtests
Made-with: Cursor
1 parent 1f54fc5 commit 3ba6044

4 files changed

Lines changed: 315 additions & 27 deletions

File tree

tools/tests/metadata_parser/metdata.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import itertools
77
from paths import PRECICE_TESTS_DIR, PRECICE_TUTORIAL_DIR
88

9+
# Import TutorialSource from systemtests.sources (used for external tutorial sources).
10+
from systemtests.sources import TutorialSource
11+
912

1013
@dataclass
1114
class BuildArgument:
@@ -279,13 +282,15 @@ def from_cases_tuple(cls, cases: Tuple[Case], tutorial: Tutorial):
279282
class ReferenceResult:
280283
path: Path
281284
case_combination: CaseCombination
285+
base_dir: Optional[Path] = None
282286

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

286290
def __post_init__(self):
287291
# built full path
288-
self.path = PRECICE_TUTORIAL_DIR / self.path
292+
base = self.base_dir if self.base_dir is not None else PRECICE_TUTORIAL_DIR
293+
self.path = Path(base) / self.path
289294

290295

291296
@dataclass
@@ -299,6 +304,7 @@ class Tutorial:
299304
url: str
300305
participants: List[str]
301306
cases: List[Case]
307+
source: "TutorialSource" = field(default_factory=TutorialSource.local)
302308
case_combinations: List[CaseCombination] = field(init=False)
303309

304310
def __post_init__(self):
@@ -355,29 +361,36 @@ def get_case_by_string(self, case_name: str) -> Optional[Case]:
355361
return None
356362

357363
@classmethod
358-
def from_yaml(cls, path, available_components):
364+
def from_yaml(cls, path, available_components, base_dir=None, source=None):
359365
"""
360366
Creates a Tutorial instance from a YAML file.
361367
362368
Args:
363-
path: The path to the YAML file.
369+
path: The path to the metadata.yaml file.
364370
available_components: The Components instance containing available components.
371+
base_dir: Optional base directory for resolving tutorial path (for external sources).
372+
Defaults to PRECICE_TUTORIAL_DIR.
373+
source: Optional TutorialSource (for external tutorials).
365374
366375
Returns:
367376
An instance of Tutorial.
368377
"""
369378
with open(path, 'r') as f:
370379
data = yaml.safe_load(f)
371380
name = data['name']
372-
path = PRECICE_TUTORIAL_DIR / data['path']
381+
base = base_dir if base_dir is not None else PRECICE_TUTORIAL_DIR
382+
tutorial_path = Path(base) / data['path']
373383
url = data['url']
374384
participants = data.get('participants', [])
375385
cases_raw = data.get('cases', {})
376386
cases = []
377387
for case_name in cases_raw.keys():
378388
cases.append(Case.from_dict(
379389
case_name, cases_raw[case_name], available_components))
380-
return cls(name, path, url, participants, cases)
390+
tut = cls(name, tutorial_path, url, participants, cases)
391+
if source is not None:
392+
tut.source = source
393+
return tut
381394

382395

383396
class Tutorials(list):
@@ -440,4 +453,4 @@ def from_path(cls, path):
440453
for yaml_path in yaml_files:
441454
tut = Tutorial.from_yaml(yaml_path, available_components)
442455
tutorials.append(tut)
443-
return cls(tutorials)
456+
return cls(tutorials)

tools/tests/systemtests/Systemtest.py

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import subprocess
2+
from .sources import resolve_tutorial_root, PRECICE_EXTERNAL_CACHE_DIR
23
from typing import List, Dict, Optional
34
from jinja2 import Environment, FileSystemLoader
45
from dataclasses import dataclass, field
@@ -299,28 +300,56 @@ def __copy_tutorial_into_directory(self, run_directory: Path):
299300
"""
300301
current_time_string = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
301302
self.run_directory = run_directory
302-
pr_requested = self.params_to_use.get("TUTORIALS_PR")
303-
if pr_requested:
304-
logging.debug(f"Fetching the PR {pr_requested} HEAD reference")
305-
self._fetch_pr(PRECICE_TUTORIAL_DIR, pr_requested)
306-
current_ref = self._get_git_ref(PRECICE_TUTORIAL_DIR)
307-
ref_requested = self.params_to_use.get("TUTORIALS_REF")
308-
if ref_requested:
309-
logging.debug(f"Checking out tutorials {ref_requested} before copying")
310-
self._fetch_ref(PRECICE_TUTORIAL_DIR, ref_requested)
311-
self._checkout_ref_in_subfolder(PRECICE_TUTORIAL_DIR, self.tutorial.path, ref_requested)
312-
313-
self.tutorial_folder = slugify(f'{self.tutorial.path.name}_{self.case_combination.cases}_{current_time_string}')
303+
304+
# Only apply PR/ref overrides for LOCAL tutorials
305+
if self.tutorial.source.type == "local":
306+
pr_requested = self.params_to_use.get("TUTORIALS_PR")
307+
if pr_requested:
308+
logging.debug(f"Fetching the PR {pr_requested} HEAD reference")
309+
self._fetch_pr(PRECICE_TUTORIAL_DIR, pr_requested)
310+
311+
current_ref = self._get_git_ref(PRECICE_TUTORIAL_DIR)
312+
313+
ref_requested = self.params_to_use.get("TUTORIALS_REF")
314+
if ref_requested:
315+
logging.debug(f"Checking out tutorials {ref_requested} before copying")
316+
self._fetch_ref(PRECICE_TUTORIAL_DIR, ref_requested)
317+
self._checkout_ref_in_subfolder(
318+
PRECICE_TUTORIAL_DIR,
319+
self.tutorial.path,
320+
ref_requested,
321+
)
322+
323+
# Create run directory name
324+
self.tutorial_folder = slugify(
325+
f"{self.tutorial.path.name}_{self.case_combination.cases}_{current_time_string}"
326+
)
327+
314328
destination = run_directory / self.tutorial_folder
315-
src = self.tutorial.path
329+
330+
# Resolve the actual tutorial root depending on source type
331+
src = resolve_tutorial_root(
332+
self.tutorial.path,
333+
self.tutorial.source,
334+
PRECICE_EXTERNAL_CACHE_DIR,
335+
)
336+
316337
self.system_test_dir = destination
317338
shutil.copytree(src, destination)
318339

319-
if ref_requested:
320-
with open(destination / "tutorials_ref", 'w') as file:
321-
file.write(ref_requested)
322-
self._checkout_ref_in_subfolder(PRECICE_TUTORIAL_DIR, self.tutorial.path, current_ref)
323-
340+
# Restore original ref if needed (local tutorials only)
341+
if self.tutorial.source.type == "local":
342+
ref_requested = self.params_to_use.get("TUTORIALS_REF")
343+
if ref_requested:
344+
with open(destination / "tutorials_ref", "w") as file:
345+
file.write(ref_requested)
346+
347+
current_ref = self._get_git_ref(PRECICE_TUTORIAL_DIR)
348+
self._checkout_ref_in_subfolder(
349+
PRECICE_TUTORIAL_DIR,
350+
self.tutorial.path,
351+
current_ref,
352+
)
324353
def __copy_tools(self, run_directory: Path):
325354
destination = run_directory / "tools"
326355
src = PRECICE_TOOLS_DIR

tools/tests/systemtests/TestSuite.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
from dataclasses import dataclass, field
2+
from pathlib import Path
23
from typing import Optional, List, Dict
3-
from metadata_parser.metdata import Tutorials, Tutorial, Case, CaseCombination, ReferenceResult
4+
from metadata_parser.metdata import (
5+
Tutorials,
6+
Tutorial,
7+
Case,
8+
CaseCombination,
9+
ReferenceResult,
10+
Components,
11+
)
12+
from paths import PRECICE_TESTS_DIR
13+
from systemtests.sources import (
14+
TutorialSource,
15+
resolve_tutorial_root,
16+
PRECICE_EXTERNAL_CACHE_DIR,
17+
)
418

519
import yaml
620

@@ -42,6 +56,7 @@ def from_yaml(cls, path, parsed_tutorials: Tutorials):
4256
An instance of TestSuites.
4357
"""
4458
testsuites = []
59+
available_components = Components.from_yaml(PRECICE_TESTS_DIR / "components.yaml")
4560
with open(path, 'r') as f:
4661
data = yaml.safe_load(f)
4762
test_suites_raw = data['test_suites']
@@ -50,7 +65,28 @@ def from_yaml(cls, path, parsed_tutorials: Tutorials):
5065
reference_results_of_tutorial = {}
5166
# iterate over tutorials:
5267
for tutorial_case in test_suites_raw[test_suite_name]['tutorials']:
68+
source = TutorialSource.from_dict(tutorial_case.get('source'))
5369
tutorial = parsed_tutorials.get_by_path(tutorial_case['path'])
70+
if not tutorial and source.type != "local":
71+
# External tutorial: fetch and load metadata
72+
tutorial_root = resolve_tutorial_root(
73+
Path(tutorial_case['path']),
74+
source,
75+
PRECICE_EXTERNAL_CACHE_DIR,
76+
)
77+
metadata_path = tutorial_root / "metadata.yaml"
78+
if not metadata_path.exists():
79+
raise FileNotFoundError(
80+
f"No metadata.yaml found for external tutorial "
81+
f"{tutorial_case['path']} at {tutorial_root}"
82+
)
83+
tutorial = Tutorial.from_yaml(
84+
metadata_path,
85+
available_components,
86+
base_dir=tutorial_root.parent,
87+
source=source,
88+
)
89+
parsed_tutorials.tutorials.append(tutorial)
5490
if not tutorial:
5591
raise Exception(f"No tutorial with path {tutorial_case['path']} found.")
5692
# initialize the datastructure for the new Testsuite
@@ -63,8 +99,12 @@ def from_yaml(cls, path, parsed_tutorials: Tutorials):
6399
tutorial_case['case_combination'], tutorial)
64100
if case_combination_requested in all_case_combinations:
65101
case_combinations_of_tutorial[tutorial].append(case_combination_requested)
102+
ref_base = tutorial.path.parent if source.type != "local" else None
66103
reference_results_of_tutorial[tutorial].append(ReferenceResult(
67-
tutorial_case['reference_result'], case_combination_requested))
104+
Path(tutorial_case['reference_result']),
105+
case_combination_requested,
106+
base_dir=ref_base,
107+
))
68108
else:
69109
raise Exception(
70110
f"Could not find the following cases {tutorial_case['case-combination']} in the current metadata of tutorial {tutorial.name}")
@@ -106,4 +146,4 @@ def __repr__(self) -> str:
106146
return_str = ""
107147
for tests_suite in self.testsuites:
108148
return_str += f"{tests_suite}\n\n"
109-
return return_str
149+
return return_str

0 commit comments

Comments
 (0)