mlcflow 1.2.5__tar.gz → 1.2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {mlcflow-1.2.5 → mlcflow-1.2.7}/PKG-INFO +1 -1
  2. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/main.py +2 -2
  3. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/repo_action.py +249 -23
  4. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/script_action.py +2 -1
  5. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlcflow.egg-info/PKG-INFO +1 -1
  6. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlcflow.egg-info/SOURCES.txt +1 -0
  7. {mlcflow-1.2.5 → mlcflow-1.2.7}/pyproject.toml +1 -1
  8. mlcflow-1.2.7/tests/test_mlcflow_unix_installer.py +83 -0
  9. mlcflow-1.2.7/tests/test_script_action_apptainer.py +54 -0
  10. mlcflow-1.2.5/tests/test_script_action_apptainer.py +0 -29
  11. {mlcflow-1.2.5 → mlcflow-1.2.7}/LICENSE.md +0 -0
  12. {mlcflow-1.2.5 → mlcflow-1.2.7}/README.md +0 -0
  13. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/__init__.py +0 -0
  14. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/__main__.py +0 -0
  15. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/action.py +0 -0
  16. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/action_factory.py +0 -0
  17. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/cache_action.py +0 -0
  18. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/cfg_action.py +0 -0
  19. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/error_codes.py +0 -0
  20. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/experiment_action.py +0 -0
  21. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/index.py +0 -0
  22. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/item.py +0 -0
  23. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/logger.py +0 -0
  24. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/meta_schema.py +0 -0
  25. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/repo.py +0 -0
  26. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlc/utils.py +0 -0
  27. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlcflow.egg-info/dependency_links.txt +0 -0
  28. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlcflow.egg-info/entry_points.txt +0 -0
  29. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlcflow.egg-info/requires.txt +0 -0
  30. {mlcflow-1.2.5 → mlcflow-1.2.7}/mlcflow.egg-info/top_level.txt +0 -0
  31. {mlcflow-1.2.5 → mlcflow-1.2.7}/setup.cfg +0 -0
  32. {mlcflow-1.2.5 → mlcflow-1.2.7}/tests/test_action_invalid_meta_entries.py +0 -0
  33. {mlcflow-1.2.5 → mlcflow-1.2.7}/tests/test_cache_mark_tmp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlcflow
3
- Version: 1.2.5
3
+ Version: 1.2.7
4
4
  Summary: An automation interface tailored for CPU/GPU benchmarking
5
5
  Author-email: MLCommons <systems@mlcommons.org>
6
6
  License:
@@ -576,7 +576,7 @@ def main():
576
576
  else:
577
577
  pre_args.target, pre_args.action = pre_args.action, None
578
578
  actions = get_action(pre_args.target, default_parent)
579
- help_text += actions.__doc__
579
+ help_text += actions.__doc__ or ""
580
580
  # iterate through every method
581
581
  for method_name, method in inspect.getmembers(
582
582
  actions.__class__, inspect.isfunction):
@@ -588,7 +588,7 @@ def main():
588
588
  action_name = pre_args.action.replace("-", "_")
589
589
  try:
590
590
  method = getattr(actions, action_name)
591
- help_text += actions.__doc__
591
+ help_text += actions.__doc__ or ""
592
592
  if method.__doc__:
593
593
  help_text += method.__doc__
594
594
  except AttributeError:
@@ -2,6 +2,7 @@ from .action import Action
2
2
  import os
3
3
  import subprocess
4
4
  import re
5
+ import shlex
5
6
  import yaml
6
7
  import json
7
8
  import shutil
@@ -39,11 +40,152 @@ class RepoAction(Action):
39
40
 
40
41
  """
41
42
 
43
+ GIT_MISSING_BRANCH_PHRASE = "did not match any file(s) known to git"
44
+ GIT_FAST_FORWARD_FAILURE_PHRASE = "not possible to fast-forward"
45
+
42
46
  def __init__(self, parent=None):
43
47
  # super().__init__(parent)
44
48
  self.parent = parent
45
49
  self.__dict__.update(vars(parent))
46
50
 
51
+ def _build_pull_command(self, repo_path, branch=None, clone_depth=None,
52
+ fast_forward_only=False):
53
+ pull_command = ['git', '-C', repo_path, 'pull']
54
+ if fast_forward_only:
55
+ pull_command.append('--ff-only')
56
+ if clone_depth is not None:
57
+ if self._is_shallow_repo(repo_path):
58
+ pull_command.extend(['--depth', str(clone_depth)])
59
+ else:
60
+ logger.warning(
61
+ f"--depth/--shallow ignored for pull on non-shallow repo {repo_path}. "
62
+ "Re-clone with --shallow to create a shallow copy."
63
+ )
64
+ # Preserve the historical existing-repo behavior of a plain
65
+ # `git pull` by default. The branch argument is only forwarded to
66
+ # `git pull` when the internal strict path explicitly opts into
67
+ # fast-forward-only updates.
68
+ if branch and fast_forward_only:
69
+ pull_command.extend(['origin', branch])
70
+ return pull_command
71
+
72
+ def _checkout_pull_branch(self, repo_path, branch):
73
+ """Ensure *branch* is checked out locally before a strict pull.
74
+
75
+ First tries to checkout an existing local branch. If that fails, fetches
76
+ the branch from origin and creates a new tracking branch. Raises
77
+ RuntimeError with contextual guidance when the branch cannot be prepared.
78
+
79
+ Args:
80
+ repo_path: Local repository path.
81
+ branch: Branch name to prepare before pulling.
82
+
83
+ Raises:
84
+ RuntimeError: If the branch cannot be fetched or checked out.
85
+ """
86
+ try:
87
+ subprocess.run(
88
+ ['git', '-C', repo_path, 'checkout', branch],
89
+ capture_output=True,
90
+ text=True,
91
+ check=True)
92
+ except subprocess.CalledProcessError as checkout_error:
93
+ checkout_error_text = self._subprocess_error_message(
94
+ checkout_error)
95
+ lowered_checkout_error = checkout_error_text.lower()
96
+ # Git uses the same exit code for many checkout failures, so we
97
+ # only fall back to fetch+track when stderr includes both
98
+ # "pathspec" and Git's usual missing-ref phrase
99
+ # "did not match any file(s) known to git".
100
+ missing_local_branch = (
101
+ "pathspec" in lowered_checkout_error
102
+ and self.GIT_MISSING_BRANCH_PHRASE in lowered_checkout_error
103
+ )
104
+ if not missing_local_branch:
105
+ raise RuntimeError(
106
+ f"Cannot switch to branch '{branch}' in {repo_path}: "
107
+ f"{checkout_error_text}"
108
+ ) from checkout_error
109
+ try:
110
+ subprocess.run(
111
+ ['git', '-C', repo_path, 'fetch', 'origin', branch],
112
+ capture_output=True,
113
+ text=True,
114
+ check=True)
115
+ except subprocess.CalledProcessError as fetch_error:
116
+ raise RuntimeError(
117
+ f"Failed to fetch branch '{branch}' in {repo_path}: "
118
+ f"{self._subprocess_error_message(fetch_error)}. "
119
+ "Ensure the branch exists on origin and that the repository is reachable."
120
+ ) from fetch_error
121
+
122
+ try:
123
+ subprocess.run(
124
+ ['git', '-C', repo_path, 'checkout', '-b',
125
+ branch, '--track', f'origin/{branch}'],
126
+ capture_output=True,
127
+ text=True,
128
+ check=True)
129
+ except subprocess.CalledProcessError as tracking_error:
130
+ raise RuntimeError(
131
+ f"Initial checkout of '{branch}' failed in {repo_path}. "
132
+ "After fetching from origin, creating a tracking branch also failed. "
133
+ f"Initial error: {checkout_error_text}. "
134
+ f"Tracking branch error: {self._subprocess_error_message(tracking_error)}. "
135
+ "Check that the branch name is correct and that your local checkout can track origin."
136
+ ) from tracking_error
137
+
138
+ @staticmethod
139
+ def _format_stash_restore_guidance(repo_path):
140
+ return (
141
+ f"Local changes remain in stash. Please run `git -C {repo_path} stash apply` "
142
+ "after resolving pull issues."
143
+ )
144
+
145
+ def _format_pull_error(self, repo_path, error_message, stash_created=False,
146
+ force=False, failure_phase="git pull"):
147
+ """Format a pull failure message.
148
+
149
+ Args:
150
+ repo_path: Repository path being updated.
151
+ error_message: Original stderr/stdout-derived git failure text.
152
+ stash_created: Whether local changes were stashed for a force pull.
153
+ force: Whether the failure happened on a force-pull path.
154
+ failure_phase: Phase label such as ``git pull`` or
155
+ ``branch checkout``.
156
+
157
+ Returns:
158
+ A user-facing error string with optional recovery guidance.
159
+ """
160
+ prefix = "Force pull failed" if force else "Pull failed"
161
+ if failure_phase:
162
+ prefix = f"{prefix} during {failure_phase}"
163
+
164
+ resolution = ""
165
+ lowered_error = error_message.lower()
166
+ # Git reports ff-only failures via stderr text instead of a distinct
167
+ # exit code, so use this case-insensitive substring from the standard
168
+ # "Not possible to fast-forward, aborting." message to add guidance.
169
+ if self.GIT_FAST_FORWARD_FAILURE_PHRASE in lowered_error:
170
+ resolution = (
171
+ " Check whether the local and remote branches have diverged "
172
+ "and reconcile them manually before retrying."
173
+ )
174
+
175
+ if stash_created:
176
+ return (
177
+ f"{prefix} for {repo_path}. "
178
+ f"{self._format_stash_restore_guidance(repo_path)} "
179
+ f"Details: {error_message}{resolution}"
180
+ )
181
+
182
+ return f"{prefix} for {repo_path}: {error_message}{resolution}"
183
+
184
+ @staticmethod
185
+ def _subprocess_error_message(error):
186
+ """Return the most helpful message from a subprocess-related error."""
187
+ return (error.stderr or error.stdout or str(error)).strip()
188
+
47
189
  def add(self, run_args):
48
190
  """
49
191
  ####################################################################################################################
@@ -322,8 +464,21 @@ class RepoAction(Action):
322
464
  return {"return": 0, "value": os.path.basename(
323
465
  url).replace(".git", "")}
324
466
 
467
+ def _is_shallow_repo(self, repo_path):
468
+ """Return True if the git repository at *repo_path* is a shallow clone."""
469
+ try:
470
+ result = subprocess.run(
471
+ ['git', '-C', repo_path, 'rev-parse', '--is-shallow-repository'],
472
+ capture_output=True,
473
+ text=True,
474
+ )
475
+ return result.returncode == 0 and result.stdout.strip() == 'true'
476
+ except (subprocess.SubprocessError, FileNotFoundError, OSError):
477
+ return False
478
+
325
479
  def pull_repo(self, repo_url, branch=None, checkout=None, tag=None,
326
- pat=None, ssh=None, ignore_on_conflict=False, repo_path=None, force=False):
480
+ pat=None, ssh=None, ignore_on_conflict=False, repo_path=None, force=False,
481
+ shallow=False, depth=None, extra_git_args=None, fast_forward_only=False):
327
482
 
328
483
  # Determine the checkout path from environment or default
329
484
  repo_base_path = self.repos_path # either the value will be from 'MLC_REPOS'
@@ -358,20 +513,41 @@ class RepoAction(Action):
358
513
  repo_path = os.path.join(repo_base_path, repo_download_name)
359
514
 
360
515
  try:
516
+ # Compute depth argument: --shallow implies depth=1; explicit
517
+ # --depth=N takes precedence
518
+ clone_depth = None
519
+ if depth is not None:
520
+ try:
521
+ clone_depth = int(depth)
522
+ except (TypeError, ValueError):
523
+ return {
524
+ "return": 1, "error": f"Invalid value for --depth: {depth!r}. Must be a positive integer."}
525
+ if clone_depth < 1:
526
+ return {
527
+ "return": 1, "error": f"Invalid value for --depth: {clone_depth}. Must be a positive integer."}
528
+ elif shallow:
529
+ clone_depth = 1
530
+
531
+ # Parse extra_git_args into a list
532
+ extra_args = []
533
+ if extra_git_args:
534
+ if isinstance(extra_git_args, list):
535
+ extra_args = extra_git_args
536
+ else:
537
+ extra_args = shlex.split(str(extra_git_args))
538
+
361
539
  # If the directory doesn't exist, clone it
362
540
  if not os.path.exists(repo_path):
363
541
  logger.info(f"Cloning repository {repo_url} to {repo_path}...")
364
542
 
365
- # Build clone command without branch if not provided
366
- clone_command = ['git', 'clone', repo_url, repo_path]
543
+ # Build clone command
544
+ clone_command = ['git', 'clone']
367
545
  if branch:
368
- clone_command = [
369
- 'git',
370
- 'clone',
371
- '--branch',
372
- branch,
373
- repo_url,
374
- repo_path]
546
+ clone_command += ['--branch', branch]
547
+ if clone_depth is not None:
548
+ clone_command += ['--depth', str(clone_depth)]
549
+ clone_command += extra_args
550
+ clone_command += [repo_url, repo_path]
375
551
 
376
552
  subprocess.run(clone_command, check=True)
377
553
 
@@ -433,21 +609,33 @@ class RepoAction(Action):
433
609
  logger.info(
434
610
  "Pulling latest changes...")
435
611
  try:
612
+ if fast_forward_only and branch:
613
+ self._checkout_pull_branch(repo_path, branch)
436
614
  subprocess.run(
437
- ['git', '-C', repo_path, 'pull'],
615
+ self._build_pull_command(
616
+ repo_path, branch, clone_depth, fast_forward_only),
438
617
  capture_output=True,
439
618
  text=True,
440
619
  check=True)
620
+ except RuntimeError as e:
621
+ return {
622
+ "return": 1,
623
+ "error": self._format_pull_error(
624
+ repo_path,
625
+ str(e),
626
+ stash_created=stash_created,
627
+ force=True,
628
+ failure_phase="branch checkout")
629
+ }
441
630
  except subprocess.CalledProcessError as e:
442
- pull_error = (e.stderr or e.stdout or str(e)).strip()
443
- if stash_created:
444
- return {
445
- "return": 1,
446
- "error": f"Force pull failed during git pull for {repo_path}. Local changes remain in stash. Please run `git -C {repo_path} stash apply` after resolving pull issues. Details: {pull_error}"
447
- }
631
+ pull_error = self._subprocess_error_message(e)
448
632
  return {
449
633
  "return": 1,
450
- "error": f"Force pull failed during git pull for {repo_path}: {pull_error}"
634
+ "error": self._format_pull_error(
635
+ repo_path,
636
+ pull_error,
637
+ stash_created=stash_created,
638
+ force=True)
451
639
  }
452
640
  logger.info("Repository successfully pulled.")
453
641
 
@@ -492,8 +680,28 @@ class RepoAction(Action):
492
680
  else:
493
681
  logger.info(
494
682
  "No local changes detected. Pulling latest changes...")
495
- subprocess.run(
496
- ['git', '-C', repo_path, 'pull'], check=True)
683
+ try:
684
+ if fast_forward_only and branch:
685
+ self._checkout_pull_branch(repo_path, branch)
686
+ subprocess.run(
687
+ self._build_pull_command(
688
+ repo_path, branch, clone_depth, fast_forward_only),
689
+ capture_output=True,
690
+ text=True,
691
+ check=True)
692
+ except RuntimeError as e:
693
+ return {
694
+ "return": 1,
695
+ "error": self._format_pull_error(
696
+ repo_path, str(e), failure_phase="branch checkout")
697
+ }
698
+ except subprocess.CalledProcessError as e:
699
+ pull_error = self._subprocess_error_message(e)
700
+ return {
701
+ "return": 1,
702
+ "error": self._format_pull_error(
703
+ repo_path, pull_error)
704
+ }
497
705
  logger.info("Repository successfully pulled.")
498
706
 
499
707
  if tag:
@@ -533,6 +741,8 @@ class RepoAction(Action):
533
741
 
534
742
  return {"return": 0}
535
743
 
744
+ except RuntimeError as e:
745
+ return {'return': 1, 'error': str(e)}
536
746
  except subprocess.CalledProcessError as e:
537
747
  return {'return': 1, 'error': f"Git command failed: {e}"}
538
748
  except Exception as e:
@@ -558,10 +768,13 @@ class RepoAction(Action):
558
768
 
559
769
 
560
770
  - `--checkout <commit_sha>`: Checks out a specific commit after cloning (applicable when the repository exists locally).
561
- - `--branch <branch_name>`: Checks out a specific branch **while cloning** a new repository.
771
+ - `--branch <branch_name>`: Checks out a specific branch while cloning a new repository. When strict fast-forward-only pulls are requested for an existing checkout, it also selects the local branch to switch to before pulling `origin/<branch>`.
562
772
  - `--tag <release_tag>`: Checks out a particular release tag.
563
773
  - `--pat <access_token>` or `--ssh`: Clones a private repository using a personal access token or SSH.
564
774
  - `--force`: For existing repositories with local tracked changes, stashes changes before pull and reapplies them after pull.
775
+ - `--shallow`: Perform a shallow clone with `--depth=1` (fastest for a fresh copy without history). For existing repos, only applied if the repo is already shallow; otherwise ignored with a warning.
776
+ - `--depth=N`: Perform a shallow clone/pull with the specified history depth (e.g. `--depth=5`). For existing repos, `--depth` is only applied when the repository is already a shallow clone; passing `--depth` to a full-history clone would corrupt it and is therefore ignored with a warning.
777
+ - `--extra_git_args=<args>`: Pass additional arguments to the `git clone` command (e.g. `--extra_git_args="--filter=blob:none"`). Only applies when cloning a new repository; not used for pull on existing repos. Accepts only trusted input — arguments are passed directly to git without further validation.
565
778
 
566
779
  Example Output:
567
780
 
@@ -592,7 +805,12 @@ class RepoAction(Action):
592
805
  repo_object.path, os.W_OK):
593
806
  repo_folder_name = os.path.basename(repo_object.path)
594
807
  res = self.pull_repo(
595
- repo_folder_name, repo_path=repo_object.path, force=run_args.get('force'))
808
+ repo_folder_name, repo_path=repo_object.path, force=run_args.get(
809
+ 'force'),
810
+ shallow=run_args.get('shallow', False),
811
+ depth=run_args.get('depth'),
812
+ extra_git_args=run_args.get('extra_git_args'),
813
+ fast_forward_only=run_args.get('fast_forward_only', False))
596
814
  if res['return'] > 0:
597
815
  return res
598
816
  else:
@@ -604,6 +822,10 @@ class RepoAction(Action):
604
822
  ssh = run_args.get('ssh')
605
823
  force = run_args.get('force')
606
824
  ignore_on_conflict = run_args.get('ignore_on_conflict')
825
+ shallow = run_args.get('shallow', False)
826
+ depth = run_args.get('depth')
827
+ extra_git_args = run_args.get('extra_git_args')
828
+ fast_forward_only = run_args.get('fast_forward_only', False)
607
829
 
608
830
  if sum(bool(var) for var in [branch, checkout, tag]) > 1:
609
831
  return {
@@ -617,7 +839,11 @@ class RepoAction(Action):
617
839
  pat,
618
840
  ssh,
619
841
  ignore_on_conflict=ignore_on_conflict,
620
- force=force)
842
+ force=force,
843
+ shallow=shallow,
844
+ depth=depth,
845
+ extra_git_args=extra_git_args,
846
+ fast_forward_only=fast_forward_only)
621
847
  if res['return'] > 0:
622
848
  return res
623
849
 
@@ -244,7 +244,8 @@ Main Script Meta:""")
244
244
  "automation": "repo",
245
245
  "action": "pull",
246
246
  "repo": "mlcommons@mlperf-automations",
247
- "branch": "dev"
247
+ "branch": "dev",
248
+ "fast_forward_only": True
248
249
  })
249
250
 
250
251
  if result['return'] == 0:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlcflow
3
- Version: 1.2.5
3
+ Version: 1.2.7
4
4
  Summary: An automation interface tailored for CPU/GPU benchmarking
5
5
  Author-email: MLCommons <systems@mlcommons.org>
6
6
  License:
@@ -26,4 +26,5 @@ mlcflow.egg-info/requires.txt
26
26
  mlcflow.egg-info/top_level.txt
27
27
  tests/test_action_invalid_meta_entries.py
28
28
  tests/test_cache_mark_tmp.py
29
+ tests/test_mlcflow_unix_installer.py
29
30
  tests/test_script_action_apptainer.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mlcflow"
7
- version = "1.2.5"
7
+ version = "1.2.7"
8
8
 
9
9
  description = "An automation interface tailored for CPU/GPU benchmarking"
10
10
  authors = [
@@ -0,0 +1,83 @@
1
+ import subprocess
2
+ import platform
3
+ import sys
4
+ from pathlib import Path
5
+
6
+
7
+ REPO_ROOT = Path(__file__).resolve().parents[1]
8
+ INSTALLER_SCRIPT = REPO_ROOT / "docs" / "install" / "mlcflow_unix_installer.sh"
9
+
10
+
11
+ def _resolve_venv_dir(tmp_path, setup_snippet):
12
+ default_dir = tmp_path / "mlcflow"
13
+ marker = "__RESULT__"
14
+ command = f"""
15
+ set -euo pipefail
16
+ source "{INSTALLER_SCRIPT}"
17
+ DEFAULT_VENV_DIR="{default_dir}"
18
+ VENV_DIR="$DEFAULT_VENV_DIR"
19
+ PY_MAJOR_MINOR="$(python3 -c 'import sys; print("{{}}.{{}}".format(*sys.version_info[:2]))')"
20
+ PY_ARCH="$(python3 -c 'import platform; print(platform.machine())')"
21
+ {setup_snippet}
22
+ resolve_default_venv_dir
23
+ echo "{marker}$VENV_DIR"
24
+ """
25
+ result = subprocess.run(
26
+ ["bash", "-lc", command],
27
+ text=True,
28
+ capture_output=True,
29
+ check=True,
30
+ )
31
+
32
+ for line in reversed(result.stdout.splitlines()):
33
+ if line.startswith(marker):
34
+ return line[len(marker):]
35
+
36
+ raise AssertionError(
37
+ f"Could not parse resolved venv path from output:\n{result.stdout}")
38
+
39
+
40
+ def test_resolve_default_venv_dir_prefers_compatible_default(tmp_path):
41
+ setup_snippet = """
42
+ python3 -m venv "$DEFAULT_VENV_DIR"
43
+ """
44
+ resolved = _resolve_venv_dir(tmp_path, setup_snippet)
45
+ assert resolved == str(tmp_path / "mlcflow")
46
+
47
+
48
+ def test_resolve_default_venv_dir_uses_suffix_for_incompatible_default(
49
+ tmp_path):
50
+ setup_snippet = """
51
+ mkdir -p "$DEFAULT_VENV_DIR"
52
+ """
53
+ resolved = _resolve_venv_dir(tmp_path, setup_snippet)
54
+ assert resolved.startswith(str(tmp_path / "mlcflow_"))
55
+ assert "_py" in resolved
56
+
57
+
58
+ def test_resolve_default_venv_dir_removes_stale_shared_env_when_default_incompatible(
59
+ tmp_path):
60
+ """When the default venv is incompatible and the suffixed venv already exists
61
+ but is itself stale/broken, the stale suffixed dir must be removed so that
62
+ setup_venv can create a fresh one rather than silently reusing a broken env."""
63
+ setup_snippet = """
64
+ mkdir -p "$DEFAULT_VENV_DIR"
65
+ SHARED_VENV_DIR="${DEFAULT_VENV_DIR}_${PY_ARCH}_py${PY_MAJOR_MINOR}"
66
+ mkdir -p "$SHARED_VENV_DIR"
67
+ """
68
+ resolved = _resolve_venv_dir(tmp_path, setup_snippet)
69
+ expected_suffix = f"{tmp_path / 'mlcflow'}_{platform.machine()}_py{sys.version_info[0]}.{sys.version_info[1]}"
70
+ assert resolved == expected_suffix
71
+ # The stale shared dir should have been removed so setup_venv creates it
72
+ # fresh
73
+ assert not Path(expected_suffix).exists()
74
+
75
+
76
+ def test_resolve_default_venv_dir_reuses_compatible_suffixed_env(tmp_path):
77
+ setup_snippet = """
78
+ SHARED_VENV_DIR="${DEFAULT_VENV_DIR}_${PY_ARCH}_py${PY_MAJOR_MINOR}"
79
+ python3 -m venv "$SHARED_VENV_DIR"
80
+ """
81
+ resolved = _resolve_venv_dir(tmp_path, setup_snippet)
82
+ expected = f"{tmp_path / 'mlcflow'}_{platform.machine()}_py{sys.version_info[0]}.{sys.version_info[1]}"
83
+ assert resolved == expected
@@ -0,0 +1,54 @@
1
+ import tempfile
2
+ import unittest
3
+ from unittest.mock import patch
4
+
5
+ from mlc.script_action import ScriptAction
6
+
7
+
8
+ class _Parent:
9
+ def __init__(self, repos_path=None):
10
+ self.repos_path = repos_path
11
+
12
+
13
+ class ScriptActionApptainerTest(unittest.TestCase):
14
+ def test_apptainer_delegates_to_script_module(self):
15
+ action = ScriptAction(_Parent(tempfile.gettempdir()))
16
+ run_args = {"tags": "detect,os"}
17
+ expected = {"return": 0}
18
+
19
+ with patch.object(
20
+ ScriptAction,
21
+ "call_script_module_function",
22
+ return_value=expected) as call_script_module_function:
23
+ result = action.apptainer(run_args)
24
+
25
+ self.assertEqual(result, expected)
26
+ call_script_module_function.assert_called_once_with(
27
+ "apptainer", run_args)
28
+
29
+ def test_auto_pull_uses_fast_forward_only_for_mlperf_automations(self):
30
+ with tempfile.TemporaryDirectory() as repos_path:
31
+ action = ScriptAction(_Parent(repos_path))
32
+
33
+ with patch.object(
34
+ ScriptAction,
35
+ "find_target_folder",
36
+ return_value=None), \
37
+ patch.object(
38
+ ScriptAction,
39
+ "access",
40
+ return_value={"return": 1, "error": "pull failed"}) as access:
41
+ result = action.call_script_module_function("run", {})
42
+
43
+ self.assertEqual(result["return"], 1)
44
+ access.assert_called_once_with({
45
+ "automation": "repo",
46
+ "action": "pull",
47
+ "repo": "mlcommons@mlperf-automations",
48
+ "branch": "dev",
49
+ "fast_forward_only": True
50
+ })
51
+
52
+
53
+ if __name__ == "__main__":
54
+ unittest.main()
@@ -1,29 +0,0 @@
1
- import unittest
2
- from unittest.mock import patch
3
-
4
- from mlc.script_action import ScriptAction
5
-
6
-
7
- class _Parent:
8
- pass
9
-
10
-
11
- class ScriptActionApptainerTest(unittest.TestCase):
12
- def test_apptainer_delegates_to_script_module(self):
13
- action = ScriptAction(_Parent())
14
- run_args = {"tags": "detect,os"}
15
- expected = {"return": 0}
16
-
17
- with patch.object(
18
- ScriptAction,
19
- "call_script_module_function",
20
- return_value=expected) as call_script_module_function:
21
- result = action.apptainer(run_args)
22
-
23
- self.assertEqual(result, expected)
24
- call_script_module_function.assert_called_once_with(
25
- "apptainer", run_args)
26
-
27
-
28
- if __name__ == "__main__":
29
- unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes