tools: make dependency retries safe
diff --git a/test/hil/test/test_change_impact.py b/test/hil/test/test_change_impact.py index 4a3631f..9cda65d 100644 --- a/test/hil/test/test_change_impact.py +++ b/test/hil/test/test_change_impact.py
@@ -3563,24 +3563,40 @@ r = subprocess.run([sys.executable, '-c', code], cwd=REPO, capture_output=True, text=True, timeout=60) self.assertEqual(r.returncode, 0, r.stderr) self.assertEqual(r.stdout.strip(), '[]') + + class TestGetDepsExistingDirectory(unittest.TestCase): - def run_get_dep(self, fail=None, create_git=True): + def run_get_dep(self, fail=None, create_git=True, initialized=False, valid=True, unborn=False, + origin=True, pinned=False): from tempfile import TemporaryDirectory from unittest import mock import get_deps - with TemporaryDirectory() as top: + with TemporaryDirectory(prefix='tinyusb deps ') as top: dep = pathlib.Path(top, 'lib', 'dep') dep.mkdir(parents=True) + if initialized: + (dep / '.git').mkdir() commands = [] def run(command): commands.append(command) - if command.endswith(' init') and fail != 'init' and create_git: + if command[-1] == 'init' and fail != 'init' and create_git: (dep / '.git').mkdir() - return subprocess.CompletedProcess([], 1 if fail and command.endswith(fail) else 0, - stdout=b'parent-head\n') + returncode, output = 0, b'parent-head\n' + if command[-1] == '--is-inside-work-tree': + returncode, output = (0, b'true\n') if valid else (1, b'not a repository\n') + elif command[-3:] == ['remote', 'get-url', 'origin']: + returncode, output = ((0, b'https://example.invalid/dep.git\n') if origin else (1, b'')) + elif command[-1] == 'HEAD': + if unborn: + returncode, output = 1, b'' + elif pinned: + output = b'0123456789abcdef\n' + if fail and command[-1].endswith(fail): + returncode = 1 + return subprocess.CompletedProcess([], returncode, stdout=output) with mock.patch.object(get_deps, 'TOP', pathlib.Path(top)), \ mock.patch.dict(get_deps.deps_all, @@ -3594,14 +3610,15 @@ def test_directory_without_git_metadata_is_initialized(self): dep, commands, status = self.run_get_dep() - git = f'git -C {dep}' + git = ['git', '-C', str(dep)] self.assertEqual(status, 0) self.assertEqual(commands, [ - f'{git} init', - f'{git} remote add origin https://example.invalid/dep.git', - f'{git} fetch --depth 1 origin 0123456789abcdef', - f'{git} checkout FETCH_HEAD', + [*git, 'init'], + [*git, 'remote', 'add', 'origin', 'https://example.invalid/dep.git'], + [*git, 'fetch', '--depth', '1', 'origin', '0123456789abcdef'], + [*git, 'checkout', 'FETCH_HEAD'], ]) + self.assertIn(' ', str(dep)) def test_git_failures_stop_dependency_setup(self): for name, suffix, count in [('init', 'init', 1), ('remote', 'dep.git', 2), @@ -3616,6 +3633,32 @@ self.assertEqual(status, 1) self.assertEqual(len(commands), 1) + def test_unborn_repository_retries_fetch(self): + dep, commands, status = self.run_get_dep(initialized=True, unborn=True, origin=False) + git = ['git', '-C', str(dep)] + self.assertEqual(status, 0) + self.assertEqual(commands, [ + [*git, 'rev-parse', '--is-inside-work-tree'], + [*git, 'remote', 'get-url', 'origin'], + [*git, 'remote', 'add', 'origin', 'https://example.invalid/dep.git'], + [*git, 'rev-parse', '--verify', 'HEAD'], + [*git, 'fetch', '--depth', '1', 'origin', '0123456789abcdef'], + [*git, 'checkout', 'FETCH_HEAD'], + ]) + + def test_pinned_branch_is_detached_and_failure_propagates(self): + for fail, expected in [(None, 0), ('--detach', 1)]: + with self.subTest(fail=fail): + dep, commands, status = self.run_get_dep(fail, initialized=True, pinned=True) + self.assertEqual(status, expected) + self.assertEqual(commands[-1], ['git', '-C', str(dep), 'checkout', '--detach']) + self.assertFalse(any('fetch' in command for command in commands)) + + def test_invalid_git_metadata_stops_setup(self): + dep, commands, status = self.run_get_dep(initialized=True, valid=False) + self.assertEqual(status, 1) + self.assertEqual(commands, [['git', '-C', str(dep), 'rev-parse', '--is-inside-work-tree']]) + if __name__ == '__main__': unittest.main(verbosity=1)
diff --git a/tools/get_deps.py b/tools/get_deps.py index 5160690..0bec83b 100755 --- a/tools/get_deps.py +++ b/tools/get_deps.py
@@ -362,8 +362,8 @@ def run_cmd(cmd): - r = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - title = f'Command Error: {cmd}' + r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + title = f'Command Error: {subprocess.list2cmdline([str(arg) for arg in cmd])}' if r.returncode != 0: print(title) print(r.stdout.decode("utf-8")) @@ -381,35 +381,38 @@ print(f'cloning {d} with {url}') p = Path(TOP / d) - git_cmd = f"git -C {p}" + git_cmd = ["git", "-C", str(p)] # Init git deps if not initialized. Checking only p.exists() lets git -C walk # up to TinyUSB's repository when an empty dependency directory is present. if not (p / '.git').exists(): p.mkdir(parents=True, exist_ok=True) - if run_cmd(f"{git_cmd} init").returncode != 0 or not (p / '.git').exists(): + if run_cmd([*git_cmd, "init"]).returncode != 0 or not (p / '.git').exists(): return 1 - if run_cmd(f"{git_cmd} remote add origin {url}").returncode != 0: + if run_cmd([*git_cmd, "remote", "add", "origin", url]).returncode != 0: return 1 head = None else: - # Check if commit is already fetched - result = run_cmd(f"{git_cmd} rev-parse HEAD") - heads = result.stdout.decode("utf-8").splitlines() - if result.returncode != 0 or not heads: + result = run_cmd([*git_cmd, "rev-parse", "--is-inside-work-tree"]) + if result.returncode != 0 or result.stdout.strip() != b"true": return 1 - head = heads[0] - if run_cmd(f"{git_cmd} reset --hard").returncode != 0: + if run_cmd([*git_cmd, "remote", "get-url", "origin"]).returncode != 0 and \ + run_cmd([*git_cmd, "remote", "add", "origin", url]).returncode != 0: + return 1 + result = run_cmd([*git_cmd, "rev-parse", "--verify", "HEAD"]) + heads = result.stdout.decode("utf-8").splitlines() + head = heads[0] if result.returncode == 0 and heads else None + if head is not None and run_cmd([*git_cmd, "reset", "--hard"]).returncode != 0: return 1 if commit != head: - if run_cmd(f"{git_cmd} fetch --depth 1 origin {commit}").returncode != 0: + if run_cmd([*git_cmd, "fetch", "--depth", "1", "origin", commit]).returncode != 0: return 1 - if run_cmd(f"{git_cmd} checkout FETCH_HEAD").returncode != 0: + if run_cmd([*git_cmd, "checkout", "FETCH_HEAD"]).returncode != 0: return 1 elif dep_head(d) is None: # a branch at the pinned commit: detach it, as dep_head() readers expect - if run_cmd(f"{git_cmd} checkout --detach").returncode != 0: + if run_cmd([*git_cmd, "checkout", "--detach"]).returncode != 0: return 1 # Remove files that conflict with TinyUSB's custom versions