Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,6 @@ docs/_static/css/fonts.css
**/CLAUDE.local.md
**/CLAUDE.*.md
**/.claude/settings.local.json

# pytest-optimizer durable state (profiling/benchmarks)
.pytest-optimizer/
17 changes: 17 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ def _pin_test_shell_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("SHELL", "/bin/sh")


@pytest.fixture(autouse=True)
def _pin_test_color_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Neutralize ``FORCE_COLOR`` / ``NO_COLOR`` for every test.

CLI color is decided live from these env vars (see
:class:`tmuxp._internal.colors.Colors`). A contributor who exports
``FORCE_COLOR`` in their shell would otherwise get ANSI-wrapped help
text, breaking the plain-text example assertions in
``tests/cli/test_help_examples.py`` and similar. Clearing both here gives
every test a deterministic, auto-detected baseline; tests that exercise
color set the vars explicitly, and ``monkeypatch`` restores the
contributor's values at teardown.
"""
monkeypatch.delenv("FORCE_COLOR", raising=False)
monkeypatch.delenv("NO_COLOR", raising=False)


@pytest.fixture(autouse=USING_ZSH, scope="session")
def zshrc(user_path: pathlib.Path) -> pathlib.Path | None:
"""Quiets ZSH default message.
Expand Down
5 changes: 5 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ default:
test *args:
uv run py.test {{ args }}

# Run tests in parallel with pytest-xdist (capped workers; -n auto can thrash)
[group: 'test']
test-parallel *args:
uv run py.test -n 8 {{ args }}

# Run tests then start continuous testing with pytest-watcher
[group: 'test']
start:
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ dev = [
"pytest-rerunfailures",
"pytest-mock",
"pytest-watcher",
"pytest-xdist",
# Coverage
"codecov",
"coverage",
Expand Down Expand Up @@ -95,6 +96,7 @@ testing = [
"pytest-rerunfailures",
"pytest-mock",
"pytest-watcher",
"pytest-xdist",
]
coverage =[
"codecov",
Expand Down Expand Up @@ -241,7 +243,7 @@ convention = "numpy"
"docs/_ext/aafig.py" = ["PTH"]

[tool.pytest.ini_options]
addopts = "--reruns=0 --tb=short --no-header --showlocals --doctest-modules"
addopts = "--reruns=0 --tb=short --no-header --showlocals --doctest-modules --strict-markers"
doctest_optionflags = "ELLIPSIS NORMALIZE_WHITESPACE"
testpaths = [
"src/tmuxp",
Expand Down
281 changes: 133 additions & 148 deletions tests/workspace/test_finders_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,151 +135,136 @@ def test_find_local_workspace_files(
assert result_relative == expected_paths


class TestFindLocalWorkspaceEdgeCases:
"""Edge case tests for local workspace discovery."""

def test_at_home_directory(
self,
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test behavior when starting at home directory."""
home = tmp_path / "home"
home.mkdir()
monkeypatch.setattr(pathlib.Path, "home", lambda: home)

(home / ".tmuxp.yaml").write_text("session_name: home\n")

result = find_local_workspace_files(home, stop_at_home=True)

assert len(result) == 1
assert result[0] == home / ".tmuxp.yaml"

def test_at_filesystem_root(
self,
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test traversal stops at filesystem root."""
# This test verifies no infinite loop at root
result = find_local_workspace_files(pathlib.Path("/"), stop_at_home=False)
# Should complete without error; result depends on system state
assert isinstance(result, list)

def test_yaml_precedence_over_json(
self,
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test .yaml is preferred when multiple formats exist."""
home = tmp_path / "home"
project = home / "project"
project.mkdir(parents=True)
monkeypatch.setattr(pathlib.Path, "home", lambda: home)

# Create both formats
(project / ".tmuxp.yaml").write_text("session_name: yaml\n")
(project / ".tmuxp.json").write_text('{"session_name": "json"}')

result = find_local_workspace_files(project, stop_at_home=True)

assert len(result) == 1
assert result[0].name == ".tmuxp.yaml"

def test_yml_precedence_over_json(
self,
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test .yml is preferred when .yaml not present but .json exists."""
home = tmp_path / "home"
project = home / "project"
project.mkdir(parents=True)
monkeypatch.setattr(pathlib.Path, "home", lambda: home)

# Create yml and json (no yaml)
(project / ".tmuxp.yml").write_text("session_name: yml\n")
(project / ".tmuxp.json").write_text('{"session_name": "json"}')

result = find_local_workspace_files(project, stop_at_home=True)

assert len(result) == 1
assert result[0].name == ".tmuxp.yml"

def test_stop_at_home_false_continues_past_home(
self,
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test stop_at_home=False continues traversal past home."""
# Create structure: /grandparent/home/project
grandparent = tmp_path / "grandparent"
home = grandparent / "home"
project = home / "project"
project.mkdir(parents=True)

monkeypatch.setattr(pathlib.Path, "home", lambda: home)

# Put config in grandparent (above home)
(grandparent / ".tmuxp.yaml").write_text("session_name: grandparent\n")
(project / ".tmuxp.yaml").write_text("session_name: project\n")

# With stop_at_home=True, should only find project config
result_stop = find_local_workspace_files(project, stop_at_home=True)
assert len(result_stop) == 1
assert "project" in str(result_stop[0])

# With stop_at_home=False, should find both
result_continue = find_local_workspace_files(project, stop_at_home=False)
assert len(result_continue) >= 2

def test_default_start_dir_uses_cwd(
self,
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test that None start_dir uses current working directory."""
home = tmp_path / "home"
project = home / "project"
project.mkdir(parents=True)
monkeypatch.setattr(pathlib.Path, "home", lambda: home)
monkeypatch.chdir(project)

(project / ".tmuxp.yaml").write_text("session_name: cwd\n")

result = find_local_workspace_files(None, stop_at_home=True)

assert len(result) == 1
assert result[0] == project / ".tmuxp.yaml"

def test_symlinked_directory(
self,
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test behavior with symlinked directories."""
home = tmp_path / "home"
real_project = home / "real_project"
real_project.mkdir(parents=True)
symlink_project = home / "symlink_project"
symlink_project.symlink_to(real_project)
monkeypatch.setattr(pathlib.Path, "home", lambda: home)

(real_project / ".tmuxp.yaml").write_text("session_name: test\n")

result = find_local_workspace_files(symlink_project, stop_at_home=True)

assert len(result) == 1


class TestLocalWorkspaceFilesConstant:
"""Tests for LOCAL_WORKSPACE_FILES constant."""

def test_constant_order(self) -> None:
"""Verify LOCAL_WORKSPACE_FILES has correct order (yaml, yml, json)."""
assert LOCAL_WORKSPACE_FILES == [".tmuxp.yaml", ".tmuxp.yml", ".tmuxp.json"]

def test_constant_is_list(self) -> None:
"""Verify LOCAL_WORKSPACE_FILES is a list."""
assert isinstance(LOCAL_WORKSPACE_FILES, list)
assert len(LOCAL_WORKSPACE_FILES) == 3
def test_at_home_directory(
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test behavior when starting at home directory."""
home = tmp_path / "home"
home.mkdir()
monkeypatch.setattr(pathlib.Path, "home", lambda: home)

(home / ".tmuxp.yaml").write_text("session_name: home\n")

result = find_local_workspace_files(home, stop_at_home=True)

assert len(result) == 1
assert result[0] == home / ".tmuxp.yaml"


def test_at_filesystem_root() -> None:
"""Test traversal stops at filesystem root."""
# This test verifies no infinite loop at root
result = find_local_workspace_files(pathlib.Path("/"), stop_at_home=False)
# Should complete without error; result depends on system state
assert isinstance(result, list)


def test_yaml_precedence_over_json(
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test .yaml is preferred when multiple formats exist."""
home = tmp_path / "home"
project = home / "project"
project.mkdir(parents=True)
monkeypatch.setattr(pathlib.Path, "home", lambda: home)

# Create both formats
(project / ".tmuxp.yaml").write_text("session_name: yaml\n")
(project / ".tmuxp.json").write_text('{"session_name": "json"}')

result = find_local_workspace_files(project, stop_at_home=True)

assert len(result) == 1
assert result[0].name == ".tmuxp.yaml"


def test_yml_precedence_over_json(
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test .yml is preferred when .yaml not present but .json exists."""
home = tmp_path / "home"
project = home / "project"
project.mkdir(parents=True)
monkeypatch.setattr(pathlib.Path, "home", lambda: home)

# Create yml and json (no yaml)
(project / ".tmuxp.yml").write_text("session_name: yml\n")
(project / ".tmuxp.json").write_text('{"session_name": "json"}')

result = find_local_workspace_files(project, stop_at_home=True)

assert len(result) == 1
assert result[0].name == ".tmuxp.yml"


def test_stop_at_home_false_continues_past_home(
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test stop_at_home=False continues traversal past home."""
# Create structure: /grandparent/home/project
grandparent = tmp_path / "grandparent"
home = grandparent / "home"
project = home / "project"
project.mkdir(parents=True)

monkeypatch.setattr(pathlib.Path, "home", lambda: home)

# Put config in grandparent (above home)
(grandparent / ".tmuxp.yaml").write_text("session_name: grandparent\n")
(project / ".tmuxp.yaml").write_text("session_name: project\n")

# With stop_at_home=True, should only find project config
result_stop = find_local_workspace_files(project, stop_at_home=True)
assert len(result_stop) == 1
assert "project" in str(result_stop[0])

# With stop_at_home=False, finds both, plus any configs above home
result_continue = find_local_workspace_files(project, stop_at_home=False)
assert len(result_continue) >= 2


def test_default_start_dir_uses_cwd(
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test that None start_dir uses current working directory."""
home = tmp_path / "home"
project = home / "project"
project.mkdir(parents=True)
monkeypatch.setattr(pathlib.Path, "home", lambda: home)
monkeypatch.chdir(project)

(project / ".tmuxp.yaml").write_text("session_name: cwd\n")

result = find_local_workspace_files(None, stop_at_home=True)

assert len(result) == 1
assert result[0] == project / ".tmuxp.yaml"


def test_symlinked_directory(
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test behavior with symlinked directories."""
home = tmp_path / "home"
real_project = home / "real_project"
real_project.mkdir(parents=True)
symlink_project = home / "symlink_project"
symlink_project.symlink_to(real_project)
monkeypatch.setattr(pathlib.Path, "home", lambda: home)

(real_project / ".tmuxp.yaml").write_text("session_name: test\n")

result = find_local_workspace_files(symlink_project, stop_at_home=True)

assert len(result) == 1


def test_local_workspace_files_constant_order() -> None:
"""Verify LOCAL_WORKSPACE_FILES has correct order (yaml, yml, json)."""
assert LOCAL_WORKSPACE_FILES == [".tmuxp.yaml", ".tmuxp.yml", ".tmuxp.json"]
6 changes: 0 additions & 6 deletions tests/workspace/test_freezer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import annotations

import logging
import time
import typing

import pytest
Expand Down Expand Up @@ -31,9 +30,6 @@ def test_freeze_config(session: Session) -> None:
builder.build(session=session)
assert session == builder.session

time.sleep(0.50)

session = session
new_config = freezer.freeze(session)

validation.validate_schema(new_config)
Expand Down Expand Up @@ -122,8 +118,6 @@ def test_freeze_logs_debug(
builder = WorkspaceBuilder(session_config=session_config, server=session.server)
builder.build(session=session)

time.sleep(0.50)

with caplog.at_level(logging.DEBUG, logger="tmuxp.workspace.freezer"):
freezer.freeze(session)

Expand Down
Loading