blob: 404ed73977277a3a6933a9a461c060acf4bc926c [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from pathlib import Path
from unittest import mock
import pytest
from click.testing import CliRunner
from airflow_breeze.commands.setup_commands import change_config
from airflow_breeze.utils import path_utils
from airflow_breeze.utils.path_utils import (
get_host_id,
get_isolated_worktree_path,
get_main_git_dir_for_worktree,
is_worktree_isolation_enabled,
)
class TestGetMainGitDirForWorktree:
"""Tests for get_main_git_dir_for_worktree detection."""
def test_returns_none_when_dot_git_is_directory(self, tmp_path):
"""Standard clone: .git is a directory, not a worktree."""
git_dir = tmp_path / ".git"
git_dir.mkdir()
with mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", tmp_path):
assert get_main_git_dir_for_worktree() is None
def test_returns_none_when_dot_git_missing(self, tmp_path):
"""No .git at all — not a git repo."""
with mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", tmp_path):
assert get_main_git_dir_for_worktree() is None
def test_returns_none_when_dot_git_file_without_gitdir_prefix(self, tmp_path):
""".git file exists but does not start with 'gitdir: '."""
git_file = tmp_path / ".git"
git_file.write_text("something unexpected\n")
with mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", tmp_path):
assert get_main_git_dir_for_worktree() is None
def test_absolute_gitdir_path(self, tmp_path):
"""Worktree with an absolute gitdir path resolves to the main .git directory."""
# Simulate: /main-repo/.git/worktrees/my-worktree
main_repo = tmp_path / "main-repo"
main_git = main_repo / ".git"
worktree_gitdir = main_git / "worktrees" / "my-worktree"
worktree_gitdir.mkdir(parents=True)
worktree_dir = tmp_path / "my-worktree"
worktree_dir.mkdir()
(worktree_dir / ".git").write_text(f"gitdir: {worktree_gitdir}\n")
with mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", worktree_dir):
result = get_main_git_dir_for_worktree()
assert result is not None
assert result == main_git
def test_relative_gitdir_path(self, tmp_path):
"""Worktree with a relative gitdir path is resolved correctly."""
# Simulate: main-repo/.git/worktrees/my-worktree
main_repo = tmp_path / "main-repo"
main_git = main_repo / ".git"
worktree_gitdir = main_git / "worktrees" / "my-worktree"
worktree_gitdir.mkdir(parents=True)
worktree_dir = tmp_path / "my-worktree"
worktree_dir.mkdir()
# Write a relative path from worktree_dir to worktree_gitdir
relative_gitdir = Path("../main-repo/.git/worktrees/my-worktree")
(worktree_dir / ".git").write_text(f"gitdir: {relative_gitdir}\n")
with mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", worktree_dir):
result = get_main_git_dir_for_worktree()
assert result is not None
assert result.resolve() == main_git.resolve()
@pytest.mark.parametrize(
"gitdir_content",
[
"gitdir: {path}/worktrees/wt\n",
"gitdir: {path}/worktrees/wt",
"gitdir: {path}/worktrees/wt \n",
],
ids=["trailing-newline", "no-trailing-newline", "extra-whitespace"],
)
def test_strips_whitespace_from_gitdir(self, tmp_path, gitdir_content):
"""Whitespace and trailing newlines are stripped from the gitdir content."""
main_repo = tmp_path / "main-repo"
main_git = main_repo / ".git"
(main_git / "worktrees" / "wt").mkdir(parents=True)
content = gitdir_content.format(path=main_git)
(tmp_path / ".git").write_text(content)
with mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", tmp_path):
result = get_main_git_dir_for_worktree()
assert result is not None
assert result == main_git
class TestWorktreeIsolationSwitch:
@pytest.mark.parametrize(
("env_value", "suppressed", "expected"),
[
pytest.param(None, False, True, id="default-enabled"),
pytest.param(None, True, False, id="disabled-by-config"),
pytest.param("true", True, True, id="env-overrides-config"),
pytest.param("false", False, False, id="env-disables"),
pytest.param("unexpected", True, False, id="unknown-env-falls-back-to-config"),
],
)
def test_isolation_reads_main_checkout_config(
self, tmp_path, monkeypatch, env_value, suppressed, expected
):
if env_value is None:
monkeypatch.delenv("BREEZE_WORKTREE_ISOLATION", raising=False)
else:
monkeypatch.setenv("BREEZE_WORKTREE_ISOLATION", env_value)
main_git = tmp_path / "main" / ".git"
main_git.mkdir(parents=True)
if suppressed:
(tmp_path / "main" / ".build").mkdir()
(tmp_path / "main" / ".build" / "suppress_worktree_isolation").touch()
with (
mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", tmp_path / "worktree"),
mock.patch(
"airflow_breeze.utils.path_utils.get_main_git_dir_for_worktree",
autospec=True,
return_value=main_git,
),
):
assert is_worktree_isolation_enabled() is expected
assert get_isolated_worktree_path() == ((tmp_path / "worktree").resolve() if expected else None)
def test_main_checkout_is_never_isolated(self, tmp_path, monkeypatch):
monkeypatch.setenv("BREEZE_WORKTREE_ISOLATION", "true")
with (
mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", tmp_path),
mock.patch(
"airflow_breeze.utils.path_utils.get_main_git_dir_for_worktree",
autospec=True,
return_value=None,
),
):
assert get_isolated_worktree_path() is None
@pytest.mark.parametrize("enable", [False, True])
def test_setup_config_toggles_isolation_for_all_worktrees(self, tmp_path, monkeypatch, enable):
monkeypatch.delenv("BREEZE_WORKTREE_ISOLATION", raising=False)
shared_build = tmp_path / "main" / ".build"
marker = shared_build / "suppress_worktree_isolation"
if enable:
shared_build.mkdir(parents=True)
marker.touch()
with (
mock.patch(
"airflow_breeze.commands.setup_commands.get_shared_build_cache_path",
autospec=True,
return_value=shared_build,
),
mock.patch(
"airflow_breeze.utils.path_utils.get_shared_build_cache_path",
autospec=True,
return_value=shared_build,
),
):
result = CliRunner().invoke(
change_config, ["--worktree-isolation" if enable else "--no-worktree-isolation"]
)
assert result.exit_code == 0, result.output
assert marker.exists() is not enable
assert f"Worktree isolation: {'enabled' if enable else 'disabled'}" in result.output
class TestGetHostId:
def test_generates_the_id_once_and_reuses_it(self, tmp_path):
host_id_path = tmp_path / "config" / "breeze" / "host-id"
with (
mock.patch.object(path_utils, "HOST_ID_PATH", host_id_path),
mock.patch.object(path_utils.socket, "gethostname", autospec=True, return_value="renamed-host"),
):
host_id = get_host_id()
get_host_id.cache_clear()
assert get_host_id() == host_id
assert host_id_path.read_text() == host_id
assert len(host_id) == 32
assert list(host_id_path.parent.iterdir()) == [host_id_path]
def test_keeps_the_id_written_by_a_concurrent_run(self, tmp_path):
host_id_path = tmp_path / "host-id"
original_link = path_utils.os.link
def link(source, destination):
Path(destination).write_text("written-first")
original_link(source, destination)
with (
mock.patch.object(path_utils, "HOST_ID_PATH", host_id_path),
mock.patch.object(path_utils.os, "link", autospec=True, side_effect=link),
):
assert get_host_id() == "written-first"
def test_falls_back_to_host_name_when_the_id_cannot_be_stored(self, tmp_path):
read_only = tmp_path / "read-only"
read_only.mkdir(mode=0o500)
with (
mock.patch.object(path_utils, "HOST_ID_PATH", read_only / "breeze" / "host-id"),
mock.patch.object(path_utils.socket, "gethostname", autospec=True, return_value="this-host"),
):
assert get_host_id() == "this-host"