blob: 189e1117229493d3283d105324f35d9d312b4b04 [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import sys
import tempfile
import unittest
from io import StringIO
from pathlib import Path
from unittest.mock import MagicMock, patch
worktree_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, worktree_dir)
import argparse
from subcommands import add as add_cmd
from subcommands import pool_add as pool_add_cmd
from subcommands import pool_remove as pool_remove_cmd
from subcommands import remove as remove_cmd
from worktree import NoFreeWorktreesError, SyncStatus, Worktree, WorktreeState
from worktree_pool import ADJECTIVES, NOUNS, WorktreePool
from worktree_printer import WorktreePrinter
class TestWorktreePool(unittest.TestCase):
def setUp(self) -> None:
self.temp_dir = tempfile.TemporaryDirectory()
self.fuchsia_dir = Path(self.temp_dir.name)
self.jiri_root = self.fuchsia_dir / ".jiri_root"
self.jiri_root.mkdir(parents=True, exist_ok=True)
self.pool = WorktreePool(fuchsia_dir=str(self.fuchsia_dir))
def tearDown(self) -> None:
self.temp_dir.cleanup()
def test_empty(self) -> None:
self.assertEqual(self.pool.get_worktrees(), [])
def test_invalid_state_transitions(self) -> None:
wt_path = self.jiri_root / "worktrees" / "wt1"
wt_path.mkdir(parents=True, exist_ok=True)
with open(self.pool.registry_file, "w") as f:
f.write(f"{wt_path}\n")
wt = self.pool.get_worktree_by_name("wt1")
self.assertEqual(wt.get_state(), WorktreeState.FREE)
# Cannot release if FREE
with self.assertRaises(RuntimeError):
wt.release_lease()
# Lease it
wt.acquire_lease(task_id="test")
self.assertEqual(wt.get_state(), WorktreeState.LEASED)
# Cannot lease again
with self.assertRaises(RuntimeError):
wt.acquire_lease(task_id="test2")
def test_get_any_free_worktree(self) -> None:
with self.assertRaises(NoFreeWorktreesError):
self.pool.get_any_free_worktree()
wt_path = self.jiri_root / "worktrees" / "wt1"
wt_path.mkdir(parents=True, exist_ok=True)
with open(self.pool.registry_file, "w") as f:
f.write(f"{wt_path}\n")
wt = self.pool.get_any_free_worktree()
self.assertEqual(wt.name, "wt1")
@patch("worktree.run_git")
def test_release_detaches_head(self, mock_run_git: MagicMock) -> None:
wt_path = self.jiri_root / "worktrees" / "wt1"
wt_path.mkdir(parents=True, exist_ok=True)
with open(self.pool.registry_file, "w") as f:
f.write(f"{wt_path}\n")
wt = self.pool.get_worktrees()[0]
wt.acquire_lease("my-task")
wt.release_lease()
mock_run_git.assert_called_once_with(
wt.path, ["checkout", "--detach"], quiet=True, check=True
)
def test_generate_random_pool_name(self) -> None:
name = self.pool._generate_random_pool_name()
self.assertIn("-", name)
adj, noun = name.split("-", 1)
self.assertIn(adj, ADJECTIVES)
self.assertIn(noun, NOUNS)
class TestAddSubcommand(unittest.TestCase):
def setUp(self) -> None:
self.temp_dir = tempfile.TemporaryDirectory()
self.fuchsia_dir = Path(self.temp_dir.name)
self.jiri_root = self.fuchsia_dir / ".jiri_root"
self.jiri_root.mkdir(parents=True, exist_ok=True)
self.pool = WorktreePool(fuchsia_dir=str(self.fuchsia_dir))
def tearDown(self) -> None:
self.temp_dir.cleanup()
def test_add_claims_slot(self) -> None:
wt_path = self.jiri_root / "worktrees" / "wt1"
wt_path.mkdir(parents=True, exist_ok=True)
self.pool.registry_file.write_text(f"{wt_path}\n")
args = argparse.Namespace(
name="my-feat", pool_name=None, sync=False, json=False
)
with patch("sys.stdout", new_callable=StringIO) as mock_out:
add_cmd.run(args, self.pool)
self.assertIn(".jiri_root/worktrees/my-feat", mock_out.getvalue())
wt = self.pool.get_worktrees()[0]
self.assertEqual(wt.get_state(), WorktreeState.LEASED)
lease = wt.get_lease_info()
assert lease is not None
self.assertEqual(lease.task_id, "my-feat")
@patch("subcommands.add.run_jiri")
def test_add_with_sync_order(self, mock_run_jiri: MagicMock) -> None:
wt_path = self.jiri_root / "worktrees" / "wt1"
wt_path.mkdir(parents=True, exist_ok=True)
self.pool.registry_file.write_text(f"{wt_path}\n")
args = argparse.Namespace(
name="my-feat", pool_name=None, sync=True, json=False
)
manager = MagicMock()
manager.attach_mock(mock_run_jiri, "mock_run_jiri")
with patch("sys.stdout", new_callable=StringIO):
add_cmd.run(args, self.pool)
expected_calls = [
unittest.mock.call.mock_run_jiri(
self.jiri_root,
["worktree", "sync", str(wt_path)],
check=True,
),
]
self.assertEqual(manager.mock_calls, expected_calls)
@patch("worktree.run_git")
def test_remove_by_task_id(self, mock_run_git: MagicMock) -> None:
wt_path = self.jiri_root / "worktrees" / "wt1"
wt_path.mkdir(parents=True, exist_ok=True)
self.pool.registry_file.write_text(f"{wt_path}\n")
wt = self.pool.get_worktrees()[0]
wt.acquire_lease("my-task-123")
args = argparse.Namespace(name="my-task-123")
remove_cmd.run(args, self.pool)
self.assertEqual(wt.get_state(), WorktreeState.FREE)
args_pool = argparse.Namespace(name="my-task-123", force=False)
with self.assertRaises(KeyError):
pool_remove_cmd.run(args_pool, self.pool)
@patch("worktree_pool.run_jiri")
@patch("sys.stderr", new_callable=StringIO)
def test_add_auto_provisions_when_no_free_slots(
self, mock_stderr: MagicMock, mock_run_jiri: MagicMock
) -> None:
from typing import Any
def mock_run_jiri_side_effect(
jiri_root: Path, args: list[str], **kwargs: Any
) -> MagicMock:
if args[0:2] == ["worktree", "add"]:
path = Path(args[2])
path.mkdir(parents=True, exist_ok=True)
with open(self.pool.registry_file, "a") as f:
f.write(f"{path}\n")
return MagicMock()
mock_run_jiri.side_effect = mock_run_jiri_side_effect
args = argparse.Namespace(
name="my-feat", pool_name=None, sync=False, json=False
)
with patch("sys.stdout", new_callable=StringIO) as mock_out:
add_cmd.run(args, self.pool)
self.assertIn(
"No free worktrees available in the pool. Provisioning a new one...",
mock_stderr.getvalue(),
)
self.assertIn(".jiri_root/worktrees/my-feat", mock_out.getvalue())
mock_run_jiri.assert_called_once()
call_args = mock_run_jiri.call_args[0][1]
self.assertEqual(call_args[0:2], ["worktree", "add"])
symlink_path = self.pool.worktrees_dir / "my-feat"
self.assertTrue(symlink_path.is_symlink())
worktrees = self.pool.get_worktrees()
self.assertEqual(len(worktrees), 1)
wt = worktrees[0]
self.assertEqual(wt.get_state(), WorktreeState.LEASED)
lease = wt.get_lease_info()
self.assertIsNotNone(lease)
assert lease is not None
self.assertEqual(lease.task_id, "my-feat")
def test_add_already_leased_task_fails(self) -> None:
wt_path1 = self.jiri_root / "worktrees" / "wt1"
wt_path2 = self.jiri_root / "worktrees" / "wt2"
wt_path1.mkdir(parents=True, exist_ok=True)
wt_path2.mkdir(parents=True, exist_ok=True)
self.pool.registry_file.write_text(f"{wt_path1}\n{wt_path2}\n")
args1 = argparse.Namespace(
name="my-feat", pool_name=None, sync=False, json=False
)
add_cmd.run(args1, self.pool)
args2 = argparse.Namespace(
name="my-feat", pool_name=None, sync=False, json=False
)
with self.assertRaises(ValueError) as context:
add_cmd.run(args2, self.pool)
self.assertIn("already active", str(context.exception))
class TestPoolAddSubcommand(unittest.TestCase):
def setUp(self) -> None:
self.temp_dir = tempfile.TemporaryDirectory()
self.fuchsia_dir = Path(self.temp_dir.name)
self.jiri_root = self.fuchsia_dir / ".jiri_root"
self.jiri_root.mkdir(parents=True, exist_ok=True)
self.pool = WorktreePool(fuchsia_dir=str(self.fuchsia_dir))
self.patcher_jiri = patch("worktree_pool.run_jiri")
self.patcher_fx = patch("subcommands.pool_add.run_fx")
self.mock_jiri = self.patcher_jiri.start()
self.mock_fx = self.patcher_fx.start()
def tearDown(self) -> None:
self.patcher_jiri.stop()
self.patcher_fx.stop()
self.temp_dir.cleanup()
def test_add_multiple_set_args(self) -> None:
wt_path = self.pool.worktrees_dir / "wt1"
self.pool.registry_file.write_text(f"{wt_path}\n")
args = argparse.Namespace(
name="wt1",
set=["core.x64 --out out/core", "workbench.arm64 --out out/wb"],
)
pool_add_cmd.run(args, self.pool)
self.assertEqual(self.mock_fx.call_count, 2)
self.pool.get_worktree_by_name("wt1")
def test_add_symlink_local_dir(self) -> None:
src_local = self.fuchsia_dir / "local"
src_local.mkdir(parents=True, exist_ok=True)
(src_local / "file.txt").write_text("hello")
wt_path = self.pool.worktrees_dir / "wt1"
args = argparse.Namespace(
name="wt1",
set=None,
symlink_local=True,
copy_local=False,
)
pool_add_cmd.run(args, self.pool)
dest_local = wt_path / "local"
self.assertTrue(dest_local.is_symlink())
self.assertEqual(dest_local.resolve(), src_local.resolve())
def test_add_copy_local_dir(self) -> None:
src_local = self.fuchsia_dir / "local"
src_local.mkdir(parents=True, exist_ok=True)
(src_local / "file.txt").write_text("hello")
wt_path = self.pool.worktrees_dir / "wt1"
args = argparse.Namespace(
name="wt1",
set=None,
symlink_local=False,
copy_local=True,
)
pool_add_cmd.run(args, self.pool)
dest_local = wt_path / "local"
self.assertTrue(dest_local.exists())
self.assertFalse(dest_local.is_symlink())
self.assertTrue((dest_local / "file.txt").exists())
self.assertEqual((dest_local / "file.txt").read_text(), "hello")
def test_add_local_dir_missing(self) -> None:
args = argparse.Namespace(
name="wt1",
set=None,
symlink_local=True,
copy_local=False,
)
with self.assertRaises(SystemExit):
pool_add_cmd.run(args, self.pool)
class TestWorktreeSelectedBuildDir(unittest.TestCase):
def setUp(self) -> None:
self.temp_dir = tempfile.TemporaryDirectory()
self.wt_path = Path(self.temp_dir.name)
self.wt = Worktree(
name="test-wt",
path=self.wt_path,
main_checkout_dir=self.wt_path / "main",
)
def tearDown(self) -> None:
self.temp_dir.cleanup()
def test_default_selected_build_dir(self) -> None:
with self.assertRaises(FileNotFoundError):
self.wt.selected_build_dir()
def test_empty_selected_build_dir(self) -> None:
fx_build_dir_file = self.wt_path / ".fx-build-dir"
fx_build_dir_file.write_text("")
with self.assertRaises(ValueError):
self.wt.selected_build_dir()
def test_custom_selected_build_dir(self) -> None:
fx_build_dir_file = self.wt_path / ".fx-build-dir"
fx_build_dir_file.write_text("out/custom-dir")
expected = (self.wt_path / "out" / "custom-dir").resolve()
self.assertEqual(self.wt.selected_build_dir(), expected)
def test_printer_highlights_selected_build_dir(self) -> None:
other_dir = self.wt_path / "out" / "other"
other_dir.mkdir(parents=True, exist_ok=True)
(other_dir / "args.gn").write_text(
'build_info_product = "core"\nbuild_info_board = "x64"\n'
)
default_symlink = self.wt_path / "out" / "default"
default_symlink.symlink_to("other")
fx_build_dir_file = self.wt_path / ".fx-build-dir"
fx_build_dir_file.write_text("out/default")
another_dir = self.wt_path / "out" / "another"
another_dir.mkdir(parents=True, exist_ok=True)
(another_dir / "args.gn").write_text(
'build_info_product = "workbench"\nbuild_info_board = "arm64"\n'
)
with patch.object(
self.wt, "get_sync_status", return_value=(SyncStatus.SYNCED, 0, 0)
):
with patch("sys.stdout", new_callable=StringIO) as mock_out:
WorktreePrinter.print_worktrees([self.wt])
output = mock_out.getvalue()
self.assertIn("out/other *:", output)
self.assertIn("out/another:", output)
self.assertNotIn("out/default", output)
self.assertNotIn("out/another *:", output)
def test_printer_highlights_selected_build_dir_with_color(self) -> None:
other_dir = self.wt_path / "out" / "other"
other_dir.mkdir(parents=True, exist_ok=True)
(other_dir / "args.gn").write_text(
'build_info_product = "core"\nbuild_info_board = "x64"\n'
)
default_symlink = self.wt_path / "out" / "default"
default_symlink.symlink_to("other")
fx_build_dir_file = self.wt_path / ".fx-build-dir"
fx_build_dir_file.write_text("out/default")
with patch.object(
self.wt, "get_sync_status", return_value=(SyncStatus.SYNCED, 0, 0)
):
with patch("utils.USE_COLORS", True):
with patch("sys.stdout", new_callable=StringIO) as mock_out:
WorktreePrinter.print_worktrees([self.wt])
output = mock_out.getvalue()
# Colors.GREEN is \033[92m, Colors.RESET is \033[0m
self.assertIn("\033[92m", output)
self.assertIn("out/other *:", output)
self.assertIn("\033[0m", output)
def test_printer_no_builds_no_trailing_newline(self) -> None:
with patch.object(
self.wt, "get_sync_status", return_value=(SyncStatus.SYNCED, 0, 0)
):
with patch("utils.USE_COLORS", False):
with patch("sys.stdout", new_callable=StringIO) as mock_out:
WorktreePrinter.print_worktrees([self.wt])
output = mock_out.getvalue()
self.assertEqual(output, "test-wt\n")
class TestCompletionChoices(unittest.TestCase):
def setUp(self) -> None:
self.temp_dir = tempfile.TemporaryDirectory()
self.fuchsia_dir = Path(self.temp_dir.name)
self.jiri_root = self.fuchsia_dir / ".jiri_root"
self.jiri_root.mkdir(parents=True, exist_ok=True)
self.pool = WorktreePool(fuchsia_dir=str(self.fuchsia_dir))
# Create physical worktree dirs
self.wt1_path = self.pool.worktrees_dir / "wt1"
self.wt2_path = self.pool.worktrees_dir / "wt2"
self.wt3_path = self.pool.worktrees_dir / "wt3"
self.wt1_path.mkdir(parents=True, exist_ok=True)
self.wt2_path.mkdir(parents=True, exist_ok=True)
self.wt3_path.mkdir(parents=True, exist_ok=True)
# Register them
with open(self.pool.registry_file, "w") as f:
f.write(f"{self.wt1_path}\n")
f.write(f"{self.wt2_path}\n")
f.write(f"{self.wt3_path}\n")
# wt1 is free
# wt2 is leased
wt2 = self.pool.get_worktree_by_name("wt2")
wt2.acquire_lease(task_id="task-wt2")
# wt3 is leased but also has leased symlink with different name
wt3 = self.pool.get_worktree_by_name("wt3")
wt3.acquire_lease(task_id="task-wt3")
# Create leased symlink for task-wt3
self.leased_link = self.pool.worktrees_dir / "task-wt3"
self.leased_link.symlink_to("wt3")
def tearDown(self) -> None:
self.temp_dir.cleanup()
def test_filter_physical_all(self) -> None:
choices = self.pool.get_completion_choices("physical_all")
self.assertEqual(sorted(choices), ["wt1", "wt2", "wt3"])
def test_filter_physical_free(self) -> None:
choices = self.pool.get_completion_choices("physical_free")
self.assertEqual(choices, ["wt1"])
def test_filter_physical_leased(self) -> None:
choices = self.pool.get_completion_choices("physical_leased")
self.assertEqual(sorted(choices), ["wt2", "wt3"])
def test_filter_leased(self) -> None:
choices = self.pool.get_completion_choices("leased")
self.assertEqual(choices, ["task-wt3"])
def test_filter_all(self) -> None:
choices = self.pool.get_completion_choices("all")
self.assertEqual(sorted(choices), ["task-wt3", "wt1", "wt2", "wt3"])
if __name__ == "__main__":
unittest.main()