From f3beedfdb29dc833c0dbd0f97586f14fcd9280f0 Mon Sep 17 00:00:00 2001 From: Jonathan Maple Date: Wed, 12 Aug 2026 18:15:26 -0400 Subject: [PATCH] [KT] Feature: Disable worktrees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CentOS 7 does VM's git is too old to support worktrees so our kernel_build.sh script doesn't work within the VM. This is possibly easily worked around however not all developers are fluent with WorkTrees. This feature makes them optional. They can be configured at the kernels.yaml level like CentOS 7 is defaulted to off and will generate a warning to the use if they use worktrees while checking out the cbr-7.9. It can also be set per kernel in the .private_repos as well. NOTE this can explode the on disk usage of the kernel, user descression advised. Additionally Claude Opus 4.6 Updated the testing and wrote new tests based on user desired tests. Claude Output: test_kernel_workspace.py (20 tests) — new file: - detect_repo_disk_mode for worktree, clone, and nonexistent - Clone: .git is directory, local branch tracks remote, origin URL is upstream - Clone idempotency: second setup() updates without error - Clone cleanup: folder removed - Clone failure: partial folder cleaned up - Worktree: .git is file, branch created in source root, cleanup removes both - Mode mismatch: both directions raise RuntimeError mentioning --override - load_from_filepath detection: use_worktree correct for both modes - cbr-7.9 default (use_worktree=False): both sub-repos are clones, cleanup works - cbr-7.9 --worktree: both sub-repos are worktrees, cleanup works - lts-9.2 default then re-checkout: worktree mode, second setup updates cleanly - lts-9.2 --no-worktree then --worktree: mismatch error, then override+rebuild works test_config.py (4 tests) — appended: - use_worktrees defaults to True when absent - use_worktrees: false parsed correctly - Non-bool string "false" raises ValueError - Key not in REQUIRED_KEYS test_kernels.py (8 tests) — appended: - use_worktree field defaults to None - Parsed from kernel dict - Settable via kernel_overrides - should_use_worktree precedence: CLI > per-kernel > global > default True - CentOS 7 warning via caplog --- kt/KT.md | 40 +++ kt/commands/checkout/command.py | 13 +- kt/commands/checkout/impl.py | 7 +- kt/data/kernels.yaml | 1 + kt/ktlib/config.py | 9 +- kt/ktlib/kernel_workspace.py | 67 ++++- kt/ktlib/kernels.py | 20 ++ tests/kt/ktlib/test_config.py | 52 ++++ tests/kt/ktlib/test_kernel_workspace.py | 347 ++++++++++++++++++++++++ tests/kt/ktlib/test_kernels.py | 95 +++++++ 10 files changed, 644 insertions(+), 7 deletions(-) create mode 100644 tests/kt/ktlib/test_kernel_workspace.py diff --git a/kt/KT.md b/kt/KT.md index 134ec09..60fc976 100644 --- a/kt/KT.md +++ b/kt/KT.md @@ -73,6 +73,46 @@ kernel_overrides: ```bash $ kt setup ``` +### Disable Worktrees +By default, kt will use git worktrees to checkout the kernel source and +appropriate dist-git repo. If you don't want to use worktrees you have several +options. Each option below will override the previous one, so if you turn off +worktrees at the kt config level you can turn them on for specific kernels later +by setting in the .private_repos.yaml or on the command line. + +NOTE: By default CentOS7 worktree is turned off due to git inside the CentOS7 +VM is too old to support worktrees. You can enable it if you want, but note +that this is a limitation of the VM. + +1. Set the global config variable. +```bash +{ + "base_path": "~/workspace/kt_test", + "kernels_dir": "~/workspace/kt_test/kernels", + "images_source_dir": "~/workspace/kt_test/images_source", + "images_dir": "~/workspace/kt_test/images", + "ssh_key": "~/.ssh/test.pub", + "user": "USER", + "use_worktrees": false +} +``` + +2. Set the per kernel config in the .private_repos.yaml file. +```yaml +kernel_overrides: + lts-9.2: + dist_git_branch: + dist_git_root: dist-git-tree-lts + use_worktree: false +``` + +3. CLI command line override. This will override the global and per kernel +config. However you will need to use this option for every command that uses +every time for that kernel version. +```bash +$ kt checkout lts-9.2 --no-worktree +``` + ## Implementation details: kt/ktlib is the place for common helpers that would be used for kt commands. diff --git a/kt/commands/checkout/command.py b/kt/commands/checkout/command.py index ef78518..b1fc656 100644 --- a/kt/commands/checkout/command.py +++ b/kt/commands/checkout/command.py @@ -35,6 +35,10 @@ \b $ kt checkout lts-9.2 -e CVE-2022-49909 Will create folder lts-9.2_CVE-2022-49909 instead of lts-9.2. +\b +$ kt checkout cbr-7.9 --no-worktree +Will not create worktrees for CentOS7 bridge. +Note: This is recommended because the git version in the CentOS7 VM is too old to support worktrees. """ @@ -61,12 +65,19 @@ type=str, help="Feature you'll be working on", ) +@click.option( + "--worktree/--no-worktree", + "use_worktree", + default=None, + help="Manually override config worktree configuration for kernel.", +) @click.argument("kernel", required=True, type=str, shell_complete=ShellCompletion.show_kernels) -def checkout(kernel, change_dir, override, cleanup, extra): +def checkout(kernel, change_dir, override, cleanup, extra, use_worktree): main( name=kernel, change_dir=change_dir, override=override, cleanup=cleanup, + use_worktree=use_worktree, extra=extra, ) diff --git a/kt/commands/checkout/impl.py b/kt/commands/checkout/impl.py index f855c64..28f1f49 100644 --- a/kt/commands/checkout/impl.py +++ b/kt/commands/checkout/impl.py @@ -7,14 +7,17 @@ from kt.ktlib.kernels import KernelsInfo -def main(name: str, change_dir: bool, cleanup: bool, override: bool, extra: str): +def main(name: str, change_dir: bool, cleanup: bool, override: bool, use_worktree: bool | None, extra: str): config = Config.load() kernels = KernelsInfo.from_yaml(config=config).kernels if name not in kernels: raise ValueError(f"Invalid param: {name} does not exist") kernel_info = kernels[name] - kernel_workspace = KernelWorkspace.load(name=name, config=config, kernel_info=kernel_info, extra=extra) + resolved = kernel_info.should_use_worktree(config=config, cli_value=use_worktree) + kernel_workspace = KernelWorkspace.load( + name=name, config=config, kernel_info=kernel_info, use_worktree=resolved, extra=extra + ) if cleanup: kernel_workspace.cleanup() return diff --git a/kt/data/kernels.yaml b/kt/data/kernels.yaml index 2d58a09..5ed0dc3 100644 --- a/kt/data/kernels.yaml +++ b/kt/data/kernels.yaml @@ -15,6 +15,7 @@ kernels: vm_image_url: https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2211.qcow2 os_variant: centos7 use_nfs: true + use_worktree: false lts-8.6: src_tree_root: kernel-src-tree diff --git a/kt/ktlib/config.py b/kt/ktlib/config.py index f9690e9..808fc41 100644 --- a/kt/ktlib/config.py +++ b/kt/ktlib/config.py @@ -34,6 +34,8 @@ class Config: ssh_key: Path user: str + use_worktrees: bool = True + DEFAULT: ClassVar = { "base_path": "~/ciq", "kernels_dir": "~/ciq/kernels", @@ -41,6 +43,7 @@ class Config: "images_dir": "~/ciq/tmp/virt-images", "ssh_key": "~/.ssh/id_ed25519_generic.pub", "user": os.environ["USER"], + "use_worktrees": True, } REQUIRED_KEYS: ClassVar = {"base_path", "kernels_dir", "images_source_dir", "images_dir", "ssh_key"} @@ -59,10 +62,14 @@ def from_str_dict(cls, data: dict[str, str]): data = {**data, "user": os.environ["USER"]} # Transform the str values to Path except for user - non_path_keys = {"user"} + non_path_keys = {"user", "use_worktrees"} new_data = {k: (Path(v).expanduser() if k not in non_path_keys else v) for k, v in data.items()} if not all(v.is_absolute() for k, v in new_data.items() if k not in non_path_keys): raise ValueError("all paths should be absolute; check your config") + use_worktrees = new_data.get("use_worktrees", None) + if use_worktrees is not None: + if not isinstance(use_worktrees, bool): + raise ValueError(f"use_worktrees expected Boolean value got [{use_worktrees}]") return cls(**new_data) diff --git a/kt/ktlib/kernel_workspace.py b/kt/ktlib/kernel_workspace.py index d18a3f1..ed6fd75 100644 --- a/kt/ktlib/kernel_workspace.py +++ b/kt/ktlib/kernel_workspace.py @@ -16,6 +16,7 @@ class RepoWorktree: remote: str remote_branch: str local_branch: str + use_worktree: bool = True @classmethod def load_from_filepath(cls, folder: Path): @@ -38,14 +39,51 @@ def load_from_filepath(cls, folder: Path): remote=remote, remote_branch=remote_branch, local_branch=local_branch, + use_worktree=(folder / ".git").is_file(), ) + @staticmethod + def detect_repo_disk_mode(directory) -> bool | None: + """ + Static method to work out if a .git exists in directory. + If the .git is a file its a part of a worktree representation + else a directory is a pure clone. + Return + True - Git WorkTree + False - Git Clone Repo + None - NOT a git repo + """ + git_path = directory / ".git" + if git_path.is_file(): + return True + if git_path.is_dir(): + return False + return None + def setup(self): """ - First run: It will create the worktree - Second run: It will update the worktree + First run: It will create the worktree / clone + Second run: It will update the worktree / clone """ + if self.folder.exists(): + disk_mode = self.detect_repo_disk_mode(self.folder) + if disk_mode is None or disk_mode != self.use_worktree: + disk_label = "worktree" if disk_mode else "clone" if disk_mode is False else "unknown" + requested_label = "worktree" if self.use_worktree else "clone" + raise RuntimeError( + f"Mode mismatch for {self.folder}:\n" + f" On disk: {disk_label}\n" + f" Requested: {requested_label}\n" + "To switch modes, clean up first then re-checkout:\n" + f" kt checkout --cleanup\n" + f" kt checkout --{'worktree' if self.use_worktree else 'no-worktree'}" + ) + self.update() + return + self._setup_worktree() if self.use_worktree else self._setup_clone() + + def _setup_worktree(self): try: remote_ref = f"{self.remote}/{self.remote_branch}" self.source_root.git.worktree( @@ -65,6 +103,18 @@ def setup(self): self.cleanup() raise e + def _setup_clone(self): + try: + logging.info(f"Cloning Full Repo {self.source_root.remotes.origin.url} to {self.folder}") + repo = Repo.clone_from(url=self.source_root.remotes.origin.url, to_path=self.folder, no_checkout=True) + repo_ref = f"origin/{self.remote_branch}" + logging.info(f"Checking out {self.local_branch} tracking to {repo_ref}") + repo.git.checkout("-b", self.local_branch, "--track", repo_ref) + + except GitCommandError as e: + self.cleanup() + raise e + def update(self): """ It will make sure the worktree is up-to-date with remote. @@ -76,6 +126,15 @@ def update(self): repo.remotes.origin.pull(rebase=True) def cleanup(self): + disk_mode = self.detect_repo_disk_mode(self.folder) + if disk_mode is True: + self._cleanup_worktree() + return + + logging.info(f"Removing Local Clone for {self.folder}") + self.folder.rmtree(ignore_errors=True) + + def _cleanup_worktree(self): # remove worktree, only if it exists try: self.source_root.git.worktree("remove", self.folder, "-f") @@ -165,7 +224,7 @@ def load_from_name(cls, kernel_workspace_name: str): return workspace @classmethod - def load(cls, name: str, config: Config, kernel_info: KernelInfo, extra: str): + def load(cls, name: str, config: Config, kernel_info: KernelInfo, use_worktree: bool, extra: str): if extra: name = name + "_" + extra @@ -184,6 +243,7 @@ def load(cls, name: str, config: Config, kernel_info: KernelInfo, extra: str): remote=default_remote, remote_branch=kernel_info.dist_git_branch, local_branch=dist_local_branch, + use_worktree=use_worktree, ) src_folder = folder / Path(Constants.SRC_TREE) @@ -197,6 +257,7 @@ def load(cls, name: str, config: Config, kernel_info: KernelInfo, extra: str): remote=default_remote, remote_branch=kernel_info.src_tree_branch, local_branch=src_local_branch, + use_worktree=use_worktree, ) return cls( diff --git a/kt/ktlib/kernels.py b/kt/ktlib/kernels.py index 2fe6882..ddb6aad 100644 --- a/kt/ktlib/kernels.py +++ b/kt/ktlib/kernels.py @@ -44,6 +44,26 @@ class KernelInfo: os_variant: str | None = None use_nfs: bool = False overridden: bool = False + use_worktree: bool | None = None + + def should_use_worktree(self, config: Config, cli_value=None) -> bool: + ret = True + if cli_value is not None: + ret = cli_value + elif self.use_worktree is not None: + ret = self.use_worktree + else: + ret = config.use_worktrees + + if self.os_variant == "centos7" and ret is True: + logging.warning( + "CentOS 7 does not support worktrees internally.\n" + "Please check your configs:\n" + f"- cli_value: {cli_value}\n" + f"- local use_worktree: {self.use_worktree}\n" + f"- config value: {config.use_worktrees}" + ) + return ret @dataclass diff --git a/tests/kt/ktlib/test_config.py b/tests/kt/ktlib/test_config.py index 3e1de18..a3adbc2 100644 --- a/tests/kt/ktlib/test_config.py +++ b/tests/kt/ktlib/test_config.py @@ -139,3 +139,55 @@ def test_config_load_from_json_proper_ssh_key(): def test_config_load_from_json_proper_user(): config = Config.from_json(CONFIG_STR) assert config.user == "testuser" + + +def test_config_use_worktrees_default_true(): + config = Config.from_json(CONFIG_STR) + assert config.use_worktrees is True + + +def test_config_use_worktrees_false(): + json_data = ( + "{" + '"base_path": "~/ciq",' + '"kernels_dir": "~/ciq/kernels",' + '"images_source_dir": "~/ciq/default_test_images",' + '"images_dir": "~/ciq/tmp/virt-images",' + '"ssh_key": "~/ciq/id_ed25519_generic.pub",' + '"user": "testuser",' + '"use_worktrees": false' + "}" + ) + config = Config.from_json(json_data) + assert config.use_worktrees is False + + +def test_config_use_worktrees_non_bool_raises(): + json_data = ( + "{" + '"base_path": "~/ciq",' + '"kernels_dir": "~/ciq/kernels",' + '"images_source_dir": "~/ciq/default_test_images",' + '"images_dir": "~/ciq/tmp/virt-images",' + '"ssh_key": "~/ciq/id_ed25519_generic.pub",' + '"user": "testuser",' + '"use_worktrees": "false"' + "}" + ) + with pytest.raises(ValueError, match="use_worktrees"): + Config.from_json(json_data) + + +def test_config_use_worktrees_not_required(): + json_data = ( + "{" + '"base_path": "~/ciq",' + '"kernels_dir": "~/ciq/kernels",' + '"images_source_dir": "~/ciq/default_test_images",' + '"images_dir": "~/ciq/tmp/virt-images",' + '"ssh_key": "~/ciq/id_ed25519_generic.pub",' + '"user": "testuser"' + "}" + ) + config = Config.from_json(json_data) + assert config.use_worktrees is True diff --git a/tests/kt/ktlib/test_kernel_workspace.py b/tests/kt/ktlib/test_kernel_workspace.py new file mode 100644 index 0000000..7554814 --- /dev/null +++ b/tests/kt/ktlib/test_kernel_workspace.py @@ -0,0 +1,347 @@ +import pytest +from git import Repo +from pathlib3x import Path + +from kt.ktlib.kernel_workspace import KernelWorkspace, RepoWorktree +from kt.ktlib.util import Constants + + +@pytest.fixture +def git_topology(tmp_path): + """ + Build the topology that mirrors production: + bare_remote — the "real" upstream (like github.com/ctrliq/kernel-src-tree) + source_root — a clone of bare_remote (what `kt setup` creates) + The kernel branch exists in bare_remote but in source_root only as + refs/remotes/origin/. + """ + tmp_path = Path(str(tmp_path)) + bare_remote = tmp_path / "bare_remote.git" + Repo.init(str(bare_remote), bare=True) + bare_repo = Repo(str(bare_remote)) + + source_root_path = tmp_path / "source_root" + source_root_repo = Repo.clone_from(str(bare_remote), str(source_root_path)) + + # Create an initial commit on main in source_root, push to bare + readme = source_root_path / "README" + readme.write_text("init") + source_root_repo.index.add(["README"]) + source_root_repo.index.commit("initial commit") + source_root_repo.remotes.origin.push("HEAD:refs/heads/main") + + # Create the kernel branch on the bare remote directly + kernel_branch = "ciqcbr7_9" + bare_repo.git.branch(kernel_branch, "main") + + # Fetch so source_root has origin/ciqcbr7_9 + source_root_repo.remotes.origin.fetch() + + kernels_dir = tmp_path / "kernels" + kernels_dir.mkdir() + + return { + "bare_remote": bare_remote, + "source_root_path": source_root_path, + "source_root_repo": source_root_repo, + "kernels_dir": kernels_dir, + "kernel_branch": kernel_branch, + "tmp_path": tmp_path, + } + + +def _make_repo_worktree(git_topology, folder_name, use_worktree): + """Helper to create a RepoWorktree with the test topology.""" + folder = git_topology["kernels_dir"] / folder_name + return RepoWorktree( + source_root=git_topology["source_root_repo"], + folder=folder, + remote="origin", + remote_branch=git_topology["kernel_branch"], + local_branch=f"{{testuser}}_{git_topology['kernel_branch']}", + use_worktree=use_worktree, + ) + + +# --- detect_repo_disk_mode --- + + +def test_detect_disk_mode_worktree(git_topology): + rw = _make_repo_worktree(git_topology, "wt_detect", use_worktree=True) + rw.setup() + assert RepoWorktree.detect_repo_disk_mode(rw.folder) is True + + +def test_detect_disk_mode_clone(git_topology): + rw = _make_repo_worktree(git_topology, "cl_detect", use_worktree=False) + rw.setup() + assert RepoWorktree.detect_repo_disk_mode(rw.folder) is False + + +def test_detect_disk_mode_nonexistent(tmp_path): + assert RepoWorktree.detect_repo_disk_mode(Path(str(tmp_path)) / "nope") is None + + +# --- Clone setup --- + + +def test_clone_setup_git_dir_is_directory(git_topology): + rw = _make_repo_worktree(git_topology, "clone_test", use_worktree=False) + rw.setup() + git_path = rw.folder / ".git" + assert git_path.is_dir() + + +def test_clone_setup_local_branch_tracks_remote(git_topology): + rw = _make_repo_worktree(git_topology, "clone_track", use_worktree=False) + rw.setup() + repo = Repo(str(rw.folder)) + assert repo.active_branch.name == f"{{testuser}}_{git_topology['kernel_branch']}" + tracking = repo.active_branch.tracking_branch() + assert tracking is not None + assert git_topology["kernel_branch"] in str(tracking) + + +def test_clone_setup_origin_is_real_remote(git_topology): + rw = _make_repo_worktree(git_topology, "clone_origin", use_worktree=False) + rw.setup() + repo = Repo(str(rw.folder)) + origin_url = repo.remotes.origin.url + assert origin_url == str(git_topology["bare_remote"]) + + +# --- Clone idempotency --- + + +def test_clone_idempotency_update(git_topology): + rw = _make_repo_worktree(git_topology, "clone_idem", use_worktree=False) + rw.setup() + + # Make a new commit on bare remote + source = git_topology["source_root_repo"] + readme = git_topology["source_root_path"] / "README" + readme.write_text("updated") + source.index.add(["README"]) + source.index.commit("second commit") + source.remotes.origin.push(f"HEAD:refs/heads/{git_topology['kernel_branch']}") + + # Re-run setup — should update, not error + rw.setup() + assert "updated" in (rw.folder / "README").read_text() + + +# --- Clone cleanup --- + + +def test_clone_cleanup_removes_folder(git_topology): + rw = _make_repo_worktree(git_topology, "clone_clean", use_worktree=False) + rw.setup() + assert rw.folder.exists() + rw.cleanup() + assert not rw.folder.exists() + + +# --- Clone failure path --- + + +def test_clone_failure_cleans_up_partial(git_topology): + rw = _make_repo_worktree(git_topology, "clone_fail", use_worktree=False) + # Break the remote branch so checkout fails + rw.remote_branch = "nonexistent_branch" + with pytest.raises(Exception): # noqa: B017 + rw.setup() + # Partial folder should be cleaned up + assert not rw.folder.exists() + + +# --- Worktree failure path --- + + +def test_worktree_failure_cleans_up_partial(git_topology): + rw = _make_repo_worktree(git_topology, "wt_fail", use_worktree=True) + rw.remote_branch = "nonexistent_branch" + with pytest.raises(Exception): # noqa: B017 + rw.setup() + assert not rw.folder.exists() + + +# --- Worktree setup --- + + +def test_worktree_setup_git_is_file(git_topology): + rw = _make_repo_worktree(git_topology, "wt_test", use_worktree=True) + rw.setup() + git_path = rw.folder / ".git" + assert git_path.is_file() + + +def test_worktree_setup_branch_in_source_root(git_topology): + rw = _make_repo_worktree(git_topology, "wt_branch", use_worktree=True) + rw.setup() + branches = [h.name for h in git_topology["source_root_repo"].heads] + assert rw.local_branch in branches + + +def test_worktree_cleanup_removes_both(git_topology): + rw = _make_repo_worktree(git_topology, "wt_clean", use_worktree=True) + rw.setup() + assert rw.folder.exists() + rw.cleanup() + assert not rw.folder.exists() + branches = [h.name for h in git_topology["source_root_repo"].heads] + assert rw.local_branch not in branches + + +# --- Mode mismatch --- + + +def test_mode_mismatch_clone_then_worktree(git_topology): + rw_clone = _make_repo_worktree(git_topology, "mismatch1", use_worktree=False) + rw_clone.setup() + rw_wt = _make_repo_worktree(git_topology, "mismatch1", use_worktree=True) + with pytest.raises(RuntimeError, match="Mode mismatch"): + rw_wt.setup() + + +def test_mode_mismatch_worktree_then_clone(git_topology): + rw_wt = _make_repo_worktree(git_topology, "mismatch2", use_worktree=True) + rw_wt.setup() + rw_clone = _make_repo_worktree(git_topology, "mismatch2", use_worktree=False) + with pytest.raises(RuntimeError, match="Mode mismatch"): + rw_clone.setup() + + +# --- load_from_filepath detection --- + + +def test_load_from_filepath_clone(git_topology): + rw = _make_repo_worktree(git_topology, "load_clone", use_worktree=False) + rw.setup() + loaded = RepoWorktree.load_from_filepath(rw.folder) + assert loaded.use_worktree is False + + +def test_load_from_filepath_worktree(git_topology): + rw = _make_repo_worktree(git_topology, "load_wt", use_worktree=True) + rw.setup() + loaded = RepoWorktree.load_from_filepath(rw.folder) + assert loaded.use_worktree is True + + +# =================================================================== +# Integration-style tests matching user's requested scenarios +# =================================================================== + + +def _make_full_workspace(git_topology, name, use_worktree): + """Build a KernelWorkspace with both dist and src sub-repos.""" + folder = git_topology["kernels_dir"] / name + branch = git_topology["kernel_branch"] + source_root = git_topology["source_root_repo"] + + dist_worktree = RepoWorktree( + source_root=source_root, + folder=folder / Constants.DIST_TREE, + remote="origin", + remote_branch=branch, + local_branch=f"{{testuser}}_{branch}_dist", + use_worktree=use_worktree, + ) + src_worktree = RepoWorktree( + source_root=source_root, + folder=folder / Constants.SRC_TREE, + remote="origin", + remote_branch=branch, + local_branch=f"{{testuser}}_{branch}_src", + use_worktree=use_worktree, + ) + return KernelWorkspace( + folder=folder, + dist_worktree=dist_worktree, + src_worktree=src_worktree, + ) + + +# --- cbr-7.9 default (use_worktree: false in yaml) --- + + +def test_cbr79_default_creates_clones(git_topology): + ws = _make_full_workspace(git_topology, "cbr-7.9", use_worktree=False) + ws.setup() + + for sub in [Constants.DIST_TREE, Constants.SRC_TREE]: + sub_path = ws.folder / sub + assert sub_path.exists() + assert (sub_path / ".git").is_dir() + + ws.cleanup() + assert not ws.folder.exists() + + +# --- cbr-7.9 --worktree (user explicitly overrides to worktree) --- + + +def test_cbr79_worktree_override_creates_worktrees(git_topology): + ws = _make_full_workspace(git_topology, "cbr-7.9-wt", use_worktree=True) + ws.setup() + + for sub in [Constants.DIST_TREE, Constants.SRC_TREE]: + sub_path = ws.folder / sub + assert sub_path.exists() + assert (sub_path / ".git").is_file() + + ws.cleanup() + assert not ws.folder.exists() + + +# --- lts-9.2 default (no use_worktree in yaml -> worktree mode) then checkout again --- + + +def test_lts92_default_worktree_then_update(git_topology): + ws = _make_full_workspace(git_topology, "lts-9.2", use_worktree=True) + ws.setup() + + for sub in [Constants.DIST_TREE, Constants.SRC_TREE]: + sub_path = ws.folder / sub + assert sub_path.exists() + assert (sub_path / ".git").is_file() + + # Second checkout — should update without error + ws.setup() + + for sub in [Constants.DIST_TREE, Constants.SRC_TREE]: + sub_path = ws.folder / sub + assert sub_path.exists() + assert (sub_path / ".git").is_file() + + ws.cleanup() + assert not ws.folder.exists() + + +# --- lts-9.2 --no-worktree then lts-9.2 --worktree (mode switch) --- + + +def test_lts92_no_worktree_then_worktree_mismatch(git_topology): + # First checkout with --no-worktree + ws_clone = _make_full_workspace(git_topology, "lts-9.2-switch", use_worktree=False) + ws_clone.setup() + + for sub in [Constants.DIST_TREE, Constants.SRC_TREE]: + sub_path = ws_clone.folder / sub + assert (sub_path / ".git").is_dir() + + # Second checkout with --worktree — should error on mismatch + ws_wt = _make_full_workspace(git_topology, "lts-9.2-switch", use_worktree=True) + with pytest.raises(RuntimeError, match="Mode mismatch"): + ws_wt.setup() + + # After cleanup + re-checkout, worktree mode works + ws_clone.cleanup() + ws_wt.setup() + + for sub in [Constants.DIST_TREE, Constants.SRC_TREE]: + sub_path = ws_wt.folder / sub + assert (sub_path / ".git").is_file() + + ws_wt.cleanup() + assert not ws_wt.folder.exists() diff --git a/tests/kt/ktlib/test_kernels.py b/tests/kt/ktlib/test_kernels.py index 9370929..1d95e07 100644 --- a/tests/kt/ktlib/test_kernels.py +++ b/tests/kt/ktlib/test_kernels.py @@ -163,3 +163,98 @@ def test_kernels_vm_image_url_present(): kernels_info = KernelsInfo.from_dict(data=data_with_pin, private_data={}, config=config) kernel_info = list(kernels_info.kernels.values())[0] assert kernel_info.vm_image_url == pinned_url + + +# --- use_worktree field --- + + +def test_kernels_use_worktree_default_none(): + config = Config.from_str_dict(Config.DEFAULT) + kernels_info = KernelsInfo.from_dict(data=data, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.use_worktree is None + + +def test_kernels_use_worktree_parsed_from_dict(): + kernels_with_flag = { + "kernel1": { + **kernels["kernel1"], + "use_worktree": False, + } + } + data_with_flag = {"common_repos": common_repos, "kernels": kernels_with_flag} + config = Config.from_str_dict(Config.DEFAULT) + + kernels_info = KernelsInfo.from_dict(data=data_with_flag, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.use_worktree is False + + +def test_kernels_use_worktree_settable_via_overrides(): + config = Config.from_str_dict(Config.DEFAULT) + overrides = {"kernel1": {"use_worktree": False}} + + kernels_info = KernelsInfo.from_dict(data=data, private_data={}, config=config, kernel_overrides=overrides) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.use_worktree is False + + +# --- should_use_worktree precedence --- + + +def test_should_use_worktree_cli_wins(): + config = Config.from_str_dict({**Config.DEFAULT, "use_worktrees": True}) + kernels_with_flag = {"kernel1": {**kernels["kernel1"], "use_worktree": True}} + data_with_flag = {"common_repos": common_repos, "kernels": kernels_with_flag} + + kernels_info = KernelsInfo.from_dict(data=data_with_flag, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.should_use_worktree(config=config, cli_value=False) is False + + +def test_should_use_worktree_per_kernel_over_global(): + config = Config.from_str_dict({**Config.DEFAULT, "use_worktrees": True}) + kernels_with_flag = {"kernel1": {**kernels["kernel1"], "use_worktree": False}} + data_with_flag = {"common_repos": common_repos, "kernels": kernels_with_flag} + + kernels_info = KernelsInfo.from_dict(data=data_with_flag, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.should_use_worktree(config=config) is False + + +def test_should_use_worktree_global_fallback(): + config = Config.from_str_dict({**Config.DEFAULT, "use_worktrees": False}) + + kernels_info = KernelsInfo.from_dict(data=data, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + # kernel1 has no use_worktree set, so falls through to global + assert kernel_info.should_use_worktree(config=config) is False + + +def test_should_use_worktree_default_true(): + config = Config.from_str_dict(Config.DEFAULT) + + kernels_info = KernelsInfo.from_dict(data=data, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + assert kernel_info.should_use_worktree(config=config) is True + + +def test_should_use_worktree_centos7_warns(caplog): + kernels_centos7 = { + "kernel1": { + **kernels["kernel1"], + "os_variant": "centos7", + } + } + data_centos7 = {"common_repos": common_repos, "kernels": kernels_centos7} + config = Config.from_str_dict(Config.DEFAULT) + + kernels_info = KernelsInfo.from_dict(data=data_centos7, private_data={}, config=config) + kernel_info = list(kernels_info.kernels.values())[0] + + import logging + + with caplog.at_level(logging.WARNING): + result = kernel_info.should_use_worktree(config=config) + assert result is True + assert "CentOS 7" in caplog.text