blob: 926a925a39d6c76cb247dd390f6338e006d9ba3f [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
import json
from build_dir import BuildDir
from subcommands import add as add_cmd
from subcommands import list as list_cmd
from subcommands import pool_add as pool_add_cmd
from subcommands import pool_list as pool_list_cmd
from subcommands import pool_remove as pool_remove_cmd
from subcommands import remove as remove_cmd
from utils import run_jiri
from worktree import NoFreeWorktreesError, SyncStatus, Worktree, WorktreeState
from worktree_pool import ADJECTIVES, NOUNS, WorktreePool
from worktree_printer import WorktreePrinter, format_time_ago_plain
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 _write_jiri_config(self, checkout: Path, body: str) -> None:
(checkout / ".jiri_root").mkdir(parents=True, exist_ok=True)
(checkout / ".jiri_root" / "config").write_text(
f"<config>{body}</config>"
)
def test_empty(self) -> None:
self.assertEqual(self.pool.get_worktrees(), [])
self.assertEqual(
self.pool.worktrees_dir, (self.jiri_root / "worktrees").resolve()
)
def test_configured_relative_worktrees_dir(self) -> None:
main = self.fuchsia_dir / "fuchsia"
self._write_jiri_config(
main, "<worktrees_dir>../fuchsia-worktrees</worktrees_dir>"
)
pool = WorktreePool(fuchsia_dir=str(main))
self.assertEqual(
pool.worktrees_dir,
(self.fuchsia_dir / "fuchsia-worktrees").resolve(),
)
def test_configured_absolute_worktrees_dir(self) -> None:
pool_dir = self.fuchsia_dir / "elsewhere" / "pool"
self._write_jiri_config(
self.fuchsia_dir, f"<worktrees_dir>{pool_dir}</worktrees_dir>"
)
pool = WorktreePool(fuchsia_dir=str(self.fuchsia_dir))
self.assertEqual(pool.worktrees_dir, pool_dir.resolve())
def test_resolves_main_checkout_from_inside_worktree(self) -> None:
main = self.fuchsia_dir / "fuchsia"
self._write_jiri_config(
main, "<worktrees_dir>../fuchsia-worktrees</worktrees_dir>"
)
wt_path = self.fuchsia_dir / "fuchsia-worktrees" / "wt1"
# A worktree's config starts as a copy of the main checkout's, so its
# own worktrees_dir may be stale; the main checkout's one wins.
self._write_jiri_config(
wt_path,
f"<worktree><parent_root>{main}</parent_root></worktree>"
"<worktrees_dir>stale</worktrees_dir>",
)
pool = WorktreePool(fuchsia_dir=str(wt_path))
self.assertEqual(pool.fuchsia_dir, main.resolve())
self.assertEqual(pool.worktrees_dir, wt_path.parent.resolve())
def test_parent_root_cycle_terminates(self) -> None:
a = self.fuchsia_dir / "a"
b = self.fuchsia_dir / "b"
self._write_jiri_config(
a, f"<worktree><parent_root>{b}</parent_root></worktree>"
)
self._write_jiri_config(
b, f"<worktree><parent_root>{a}</parent_root></worktree>"
)
pool = WorktreePool(fuchsia_dir=str(a))
self.assertEqual(pool.fuchsia_dir, a.resolve())
def test_get_worktrees_ignores_entries_outside_pool(self) -> None:
wt_path = self.jiri_root / "worktrees" / "wt1"
wt_path.mkdir(parents=True, exist_ok=True)
external_path = self.fuchsia_dir / "external-wt"
external_path.mkdir(parents=True, exist_ok=True)
with open(self.pool.registry_file, "w") as f:
f.write(f"{wt_path}\n{external_path}\n")
self.assertEqual(
[wt.path for wt in self.pool.get_worktrees()], [wt_path]
)
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
)
@patch("worktree.run_jj")
@patch("worktree.run_git")
def test_release_renames_jj_workspaces(
self,
mock_run_git: MagicMock,
mock_run_jj: MagicMock,
) -> None:
wt_path = self.jiri_root / "worktrees" / "wt1"
# The root and the vendor/google sub-project are jj workspaces; the
# integration sub-project is a plain git worktree.
for rel in (".", "vendor/google"):
(wt_path / rel / ".jj").mkdir(parents=True, exist_ok=True)
(wt_path / rel / ".jj" / "repo").write_text("../../../.jj/repo\n")
(wt_path / "integration").mkdir(parents=True, exist_ok=True)
snapshot = self.jiri_root / "update_history" / "latest"
snapshot.parent.mkdir(parents=True, exist_ok=True)
snapshot.write_text(
"<manifest><projects>"
'<project name="fuchsia" path="."/>'
'<project name="vendor/google" path="vendor/google"/>'
'<project name="integration" path="integration"/>'
"</projects></manifest>"
)
with open(self.pool.registry_file, "w") as f:
f.write(f"{wt_path}\n")
wt = self.pool.get_worktrees()[0]
self.assertTrue(wt.is_jj_workspace())
self.assertEqual(
wt.jj_workspace_paths(),
[wt_path.resolve(), (wt_path / "vendor" / "google").resolve()],
)
wt.acquire_lease("my-task")
mock_run_jj.assert_not_called()
wt.release_lease()
self.assertEqual(
[c.args for c in mock_run_jj.call_args_list],
[
(wt_path.resolve(), ["workspace", "rename", "wt1"]),
(
(wt_path / "vendor" / "google").resolve(),
["workspace", "rename", "wt1"],
),
],
)
# jj keeps HEAD detached, so git isn't needed.
mock_run_git.assert_not_called()
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()
@patch("subcommands.add.run_jiri")
def test_add_claims_slot_and_syncs(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, 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")
mock_run_jiri.assert_called_once_with(
self.jiri_root,
[
"worktree",
"sync",
"--force",
str(wt_path),
],
check=True,
)
@patch("subcommands.add.run_jiri")
def test_add_with_configured_worktrees_dir(
self, mock_run_jiri: MagicMock
) -> None:
main = self.fuchsia_dir / "fuchsia"
(main / ".jiri_root").mkdir(parents=True, exist_ok=True)
(main / ".jiri_root" / "config").write_text(
"<config><worktrees_dir>../fuchsia-worktrees</worktrees_dir></config>"
)
pool = WorktreePool(fuchsia_dir=str(main))
wt_path = pool.worktrees_dir / "wt1"
wt_path.mkdir(parents=True, exist_ok=True)
pool.registry_file.write_text(f"{wt_path}\n")
args = argparse.Namespace(name="my-feat", pool_name=None, json=False)
with patch("sys.stdout", new_callable=StringIO) as mock_out:
add_cmd.run(args, pool)
self.assertIn("'../fuchsia-worktrees/my-feat'", mock_out.getvalue())
self.assertEqual(
(self.fuchsia_dir / "fuchsia-worktrees" / "my-feat").readlink(),
Path("wt1"),
)
@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("subcommands.add.run_jiri")
@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_pool: MagicMock,
mock_run_jiri_add: 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_pool.side_effect = mock_run_jiri_side_effect
args = argparse.Namespace(name="my-feat", pool_name=None, 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_pool.assert_called_once()
call_args = mock_run_jiri_pool.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")
mock_run_jiri_add.assert_called_once_with(
self.jiri_root,
[
"worktree",
"sync",
"--force",
str(wt.path),
],
check=True,
)
@patch("subcommands.add.run_jiri")
def test_add_already_leased_task_fails(
self, mock_run_jiri: MagicMock
) -> 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, json=False)
add_cmd.run(args1, self.pool)
args2 = argparse.Namespace(name="my-feat", pool_name=None, 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"])
class TestListSubcommand(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_list_json_output(self) -> None:
wt1_path = self.pool.worktrees_dir / "wt1"
wt2_path = self.pool.worktrees_dir / "wt2"
wt3_path = self.pool.worktrees_dir / "wt3"
wt1_path.mkdir(parents=True, exist_ok=True)
wt2_path.mkdir(parents=True, exist_ok=True)
wt3_path.mkdir(parents=True, exist_ok=True)
self.pool.registry_file.write_text(
f"{wt1_path}\n{wt2_path}\n{wt3_path}\n"
)
# Lease in reverse order to test alphabetical sorting by task name
wt1 = self.pool.get_worktree_by_name("wt1")
wt1.acquire_lease("zeta-task")
wt2 = self.pool.get_worktree_by_name("wt2")
wt2.acquire_lease("alpha-task")
# Configure two build dirs on wt1: one active, one inactive
bd_active = wt1_path / "out" / "core.x64"
bd_active.mkdir(parents=True, exist_ok=True)
(bd_active / "args.gn").write_text(
'build_info_product = "core"\nbuild_info_board = "x64"\n'
)
fx_build_dir_file = wt1_path / ".fx-build-dir"
fx_build_dir_file.write_text("out/core.x64")
bd_inactive = wt1_path / "out" / "arm.x64"
bd_inactive.mkdir(parents=True, exist_ok=True)
(bd_inactive / "args.gn").write_text(
'build_info_product = "arm"\nbuild_info_board = "x64"\n'
)
def mock_time_ago(self_bd: BuildDir) -> float | None:
if "core.x64" in str(self_bd.path):
return 120.0
return None
args = argparse.Namespace(json=True)
with (
patch.object(
Worktree, "get_sync_status", return_value=(SyncStatus.NEW, 0, 3)
),
patch.object(
BuildDir,
"get_build_time_ago_sec",
autospec=True,
side_effect=mock_time_ago,
),
):
with patch("sys.stdout", new_callable=StringIO) as mock_out:
list_cmd.run(args, self.pool)
raw_output = mock_out.getvalue().strip()
data = json.loads(raw_output)
self.assertEqual(len(data), 2)
self.assertNotIn("wt3", [d["name"] for d in data])
# Insertion order in registry: wt1 (zeta-task), wt2 (alpha-task)
self.assertEqual(data[0]["name"], "zeta-task")
self.assertEqual(data[0]["task"], "zeta-task")
self.assertEqual(data[0]["sync"], {"behind": 0, "new": 3})
self.assertEqual(len(data[0]["build_dirs"]), 2)
bd0 = data[0]["build_dirs"][0]
self.assertEqual(bd0["path"], "out/arm.x64")
self.assertFalse(bd0["active"])
self.assertEqual(bd0["config"], "arm.x64")
self.assertEqual(bd0["last_build"], "never built")
bd1 = data[0]["build_dirs"][1]
self.assertEqual(bd1["path"], "out/core.x64")
self.assertTrue(bd1["active"])
self.assertEqual(bd1["config"], "core.x64")
self.assertEqual(bd1["last_build"], "2m ago")
self.assertEqual(data[1]["name"], "alpha-task")
self.assertEqual(data[1]["task"], "alpha-task")
self.assertEqual(data[1]["sync"], {"behind": 0, "new": 3})
self.assertEqual(data[1]["build_dirs"], [])
class TestPoolListSubcommand(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_pool_list_json_output(self) -> None:
wt1_path = self.pool.worktrees_dir / "wt1"
wt2_path = self.pool.worktrees_dir / "wt2"
wt1_path.mkdir(parents=True, exist_ok=True)
wt2_path.mkdir(parents=True, exist_ok=True)
self.pool.registry_file.write_text(f"{wt1_path}\n{wt2_path}\n")
# Configure build dir on wt1
bd_path = wt1_path / "out" / "core.x64"
bd_path.mkdir(parents=True, exist_ok=True)
(bd_path / "args.gn").write_text(
'build_info_product = "core"\nbuild_info_board = "x64"\n'
)
(wt1_path / ".fx-build-dir").write_text("out/core.x64")
wt2 = self.pool.get_worktree_by_name("wt2")
wt2.acquire_lease("leased-task")
args = argparse.Namespace(json=True)
with patch.object(
Worktree, "get_sync_status", return_value=(SyncStatus.SYNCED, 0, 0)
):
with patch("sys.stdout", new_callable=StringIO) as mock_out:
pool_list_cmd.run(args, self.pool)
raw_output = mock_out.getvalue().strip()
data = json.loads(raw_output)
self.assertEqual(len(data), 2)
self.assertEqual(data[0]["name"], "wt1")
self.assertIsNone(data[0]["task"])
self.assertEqual(data[0]["sync"], {"behind": 0, "new": 0})
self.assertEqual(len(data[0]["build_dirs"]), 1)
bd = data[0]["build_dirs"][0]
self.assertEqual(bd["path"], "out/core.x64")
self.assertTrue(bd["active"])
self.assertEqual(bd["config"], "core.x64")
self.assertEqual(bd["last_build"], "never built")
self.assertEqual(data[1]["name"], "wt2")
self.assertEqual(data[1]["task"], "leased-task")
self.assertEqual(data[1]["sync"], {"behind": 0, "new": 0})
self.assertEqual(data[1]["build_dirs"], [])
class TestFormatTimeAgo(unittest.TestCase):
def test_format_time_ago_plain_intervals(self) -> None:
self.assertEqual(format_time_ago_plain(None), "never built")
self.assertEqual(format_time_ago_plain(30), "just now")
self.assertEqual(format_time_ago_plain(120), "2m ago")
self.assertEqual(format_time_ago_plain(7200), "2h ago")
self.assertEqual(format_time_ago_plain(172800), "2d ago")
class TestRunJiri(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 / "bin").mkdir(parents=True, exist_ok=True)
self.jiri_bin = self.jiri_root / "bin" / "jiri"
self.jiri_bin.write_text("")
def tearDown(self) -> None:
self.temp_dir.cleanup()
@patch("utils.subprocess.run")
def test_run_jiri_defaults_cwd_to_jiri_root_parent(
self, mock_run: MagicMock
) -> None:
with patch("sys.stdout", new_callable=StringIO):
run_jiri(self.jiri_root, ["worktree", "add", "/tmp/wt"], check=True)
mock_run.assert_called_once_with(
[str(self.jiri_bin), "worktree", "add", "/tmp/wt"],
check=True,
cwd=str(self.fuchsia_dir),
)
@patch("utils.subprocess.run")
def test_run_jiri_preserves_explicit_cwd(self, mock_run: MagicMock) -> None:
with patch("sys.stdout", new_callable=StringIO):
run_jiri(
self.jiri_root,
["worktree", "sync"],
cwd="/custom/cwd",
check=True,
)
mock_run.assert_called_once_with(
[str(self.jiri_bin), "worktree", "sync"],
cwd="/custom/cwd",
check=True,
)
if __name__ == "__main__":
unittest.main()