From 9b2fd3196fa11c4e965bf40888c19e690e8f007e Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Oct 2022 07:14:23 -0500 Subject: [PATCH 1/7] test fixtures: Add tmuxp configdir --- conftest.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/conftest.py b/conftest.py index 8a628123218..a9c51dffb9f 100644 --- a/conftest.py +++ b/conftest.py @@ -15,6 +15,7 @@ from libtmux.test import namer from tests.fixtures import utils as test_utils +from tmuxp.cli.utils import get_config_dir if t.TYPE_CHECKING: from libtmux.session import Session @@ -40,6 +41,24 @@ def home_path_default(monkeypatch: pytest.MonkeyPatch, user_path: pathlib.Path) monkeypatch.setenv("HOME", str(user_path)) +@pytest.fixture +def tmuxp_configdir(user_path: pathlib.Path) -> pathlib.Path: + xdg_config_dir = user_path / ".config" + xdg_config_dir.mkdir(exist_ok=True) + + tmuxp_configdir = xdg_config_dir / "tmuxp" + tmuxp_configdir.mkdir(exist_ok=True) + return tmuxp_configdir + + +@pytest.fixture +def tmuxp_configdir_default( + monkeypatch: pytest.MonkeyPatch, tmuxp_configdir: pathlib.Path +) -> None: + monkeypatch.setenv("TMUXP_CONFIGDIR", str(tmuxp_configdir)) + assert get_config_dir() == str(tmuxp_configdir) + + @pytest.fixture(scope="function") def monkeypatch_plugin_test_packages(monkeypatch: pytest.MonkeyPatch) -> None: paths = [ From 14f04fe4c9c711d17f2ec279da14ecb173ba4d80 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 27 Oct 2022 21:13:30 -0500 Subject: [PATCH 2/7] tests(cli): Start test grid for tmuxp load args --- tests/test_cli.py | 130 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 024aa8250fd..c0b41d84332 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -433,6 +433,136 @@ def test_load_symlinked_workspace( assert pane.current_path == str(realtemp) +if t.TYPE_CHECKING: + from typing_extensions import TypeAlias + + ExpectedOutput: TypeAlias = t.Optional[t.Union[str, t.List[str]]] + + +class CLILoadFixture(t.NamedTuple): + test_id: str + cli_args: t.List[str] + config_paths: t.List[str] + expected_exit_code: int + expected_in_out: "ExpectedOutput" = None + expected_not_in_out: "ExpectedOutput" = None + expected_in_err: "ExpectedOutput" = None + expected_not_in_err: "ExpectedOutput" = None + + +TEST_LOAD_FIXTURES = [ + CLILoadFixture( + test_id="dir-relative-dot-samedir", + cli_args=["load", "."], + config_paths=["{tmp_path}/.tmuxp.yaml"], + expected_exit_code=0, + expected_in_out=None, + expected_not_in_out=None, + ), + CLILoadFixture( + test_id="dir-relative-dot-slash-samedir", + cli_args=["load", "./"], + config_paths=["{tmp_path}/.tmuxp.yaml"], + expected_exit_code=0, + expected_in_out=None, + expected_not_in_out=None, + ), + CLILoadFixture( + test_id="dir-relative-file-samedir", + cli_args=["load", "./.tmuxp.yaml"], + config_paths=["{tmp_path}/.tmuxp.yaml"], + expected_exit_code=0, + expected_in_out=None, + expected_not_in_out=None, + ), + CLILoadFixture( + test_id="filename-relative-file-samedir", + cli_args=["load", "./my_config.yaml"], + config_paths=["{tmp_path}/my_config.yaml"], + expected_exit_code=0, + expected_in_out=None, + expected_not_in_out=None, + ), + CLILoadFixture( + test_id="configdir-session-name", + cli_args=["load", "my_config"], + config_paths=["{TMUXP_CONFIGDIR}/my_config.yaml"], + expected_exit_code=0, + expected_in_out=None, + expected_not_in_out=None, + ), + CLILoadFixture( + test_id="configdir-absolute", + cli_args=["load", "~/.config/tmuxp/my_config.yaml"], + config_paths=["{TMUXP_CONFIGDIR}/my_config.yaml"], + expected_exit_code=0, + expected_in_out=None, + expected_not_in_out=None, + ), +] + + +@pytest.mark.parametrize( + list(CLILoadFixture._fields), + TEST_LOAD_FIXTURES, + ids=[test.test_id for test in TEST_LOAD_FIXTURES], +) +@pytest.mark.usefixtures("tmuxp_configdir_default") +def test_load( + tmp_path: pathlib.Path, + tmuxp_configdir: pathlib.Path, + server: "Server", + session: Session, + capsys: pytest.CaptureFixture, + monkeypatch: pytest.MonkeyPatch, + test_id: str, + cli_args: t.List[str], + config_paths: t.List[str], + expected_exit_code: int, + expected_in_out: "ExpectedOutput", + expected_not_in_out: "ExpectedOutput", + expected_in_err: "ExpectedOutput", + expected_not_in_err: "ExpectedOutput", +) -> None: + assert server.socket_name is not None + + monkeypatch.chdir(tmp_path) + for config_path in config_paths: + tmuxp_config = pathlib.Path( + config_path.format(tmp_path=tmp_path, TMUXP_CONFIGDIR=tmuxp_configdir) + ) + tmuxp_config.write_text( + """ + session_name: test + windows: + - window_name: test + panes: + - + """, + encoding="utf-8", + ) + + try: + cli.cli([*cli_args, "-d", "-L", server.socket_name]) + except SystemExit: + pass + + result = capsys.readouterr() + output = "".join(list(result.out)) + + if expected_in_out is not None: + if isinstance(expected_in_out, str): + expected_in_out = [expected_in_out] + for needle in expected_in_out: + assert needle in output + + if expected_not_in_out is not None: + if isinstance(expected_not_in_out, str): + expected_not_in_out = [expected_not_in_out] + for needle in expected_not_in_out: + assert needle not in output + + def test_regression_00132_session_name_with_dots( tmp_path: pathlib.Path, server: "Server", From e6c3d41f7594baa787bbeda91260ca9c954c12b6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Oct 2022 07:26:18 -0500 Subject: [PATCH 3/7] ci(mypy): Add types for StrPath No TypeAlias available until 3.10, unless we want to including typing_extensions. --- src/tmuxp/types.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/tmuxp/types.py diff --git a/src/tmuxp/types.py b/src/tmuxp/types.py new file mode 100644 index 00000000000..536225c9f1d --- /dev/null +++ b/src/tmuxp/types.py @@ -0,0 +1,14 @@ +"""Internal :term:`type annotations ` + +Notes +----- + +:class:`StrPath` and :class:`StrOrBytesPath` is based on `typeshed's`_. + +.. _typeshed's: https://github.com/python/typeshed/blob/9687d5/stdlib/_typeshed/__init__.pyi#L98 +""" # NOQA E501 +from os import PathLike +from typing import Union + +StrPath = Union[str, "PathLike[str]"] +""":class:`os.PathLike` or :class:`str`""" From aaa562cb0ee3b92b44bdf4522553509648cadfcb Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Oct 2022 08:07:00 -0500 Subject: [PATCH 4/7] fix(load): Accept multiple workspaces --- src/tmuxp/cli/load.py | 60 +++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/tmuxp/cli/load.py b/src/tmuxp/cli/load.py index ec016c9fa9a..85f19dfe896 100644 --- a/src/tmuxp/cli/load.py +++ b/src/tmuxp/cli/load.py @@ -16,6 +16,7 @@ from libtmux.common import has_gte_version from libtmux.server import Server from libtmux.session import Session +from tmuxp.types import StrPath from .. import config, config_reader, exc, log, util from ..workspacebuilder import WorkspaceBuilder @@ -29,13 +30,17 @@ ) if t.TYPE_CHECKING: - from typing_extensions import Literal, TypeAlias + from typing_extensions import Literal, NotRequired, TypeAlias, TypedDict CLIColorsLiteral: TypeAlias = Literal[56, 88] + class OptionOverrides(TypedDict): + detached: NotRequired[bool] + new_session_name: NotRequired[t.Optional[str]] + class CLILoadNamespace(argparse.Namespace): - config_file: str + config_files: t.List[str] socket_name: t.Optional[str] socket_path: t.Optional[str] tmux_config_file: t.Optional[str] @@ -251,7 +256,7 @@ def _setup_plugins(builder: WorkspaceBuilder) -> Session: def load_workspace( - config_file: t.Union[pathlib.Path, str], + config_file: StrPath, socket_name: t.Optional[str] = None, socket_path: None = None, tmux_config_file: None = None, @@ -266,8 +271,8 @@ def load_workspace( Parameters ---------- - config_file : str - absolute path to config file + config_file : list of str + paths or session names to workspace files socket_name : str, optional ``tmux -L `` socket_path: str, optional @@ -356,7 +361,7 @@ def load_workspace( Accessed April 8th, 2018. """ # get the canonical path, eliminating any symlinks - if isinstance(config_file, str): + if isinstance(config_file, (str, os.PathLike)): config_file = pathlib.Path(config_file) tmuxp_echo( @@ -486,8 +491,9 @@ def config_file_completion(ctx, params, incomplete): def create_load_subparser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - config_file = parser.add_argument( - "config_file", + config_files = parser.add_argument( + "config_files", + nargs="+", metavar="config-file", help="filepath to session or filename of session if in tmuxp config directory", ) @@ -568,7 +574,7 @@ def create_load_subparser(parser: argparse.ArgumentParser) -> argparse.ArgumentP try: import shtab - config_file.complete = shtab.FILE # type: ignore + config_files.complete = shtab.FILE # type: ignore tmux_config_file.complete = shtab.FILE # type: ignore log_file.complete = shtab.FILE # type: ignore except ImportError: @@ -623,27 +629,31 @@ def command_load( "append": args.append, } - if args.config_file is None: + if args.config_files is None or len(args.config_files) == 0: tmuxp_echo("Enter at least one config") if parser is not None: parser.print_help() sys.exit() + return - config_file = scan_config(args.config_file, config_dir=get_config_dir()) + last_idx = len(args.config_files) - 1 + original_options = tmux_options.copy() - if isinstance(config_file, str): - load_workspace(config_file, **tmux_options) - elif isinstance(config_file, tuple): - config = list(config_file) - # Load each configuration but the last to the background - for cfg in config[:-1]: - opt = tmux_options.copy() - opt.update({"detached": True, "new_session_name": None}) - load_workspace(cfg, **opt) + for idx, config_file in enumerate(args.config_files): + config_file = scan_config(config_file, config_dir=get_config_dir()) - # todo: obey the -d in the cli args only if user specifies - load_workspace(config_file[-1], **tmux_options) - else: - raise NotImplementedError( - f"config {type(config_file)} with {config_file} not valid" + detached = tmux_options.pop("detached", original_options.get("detached", False)) + new_session_name = tmux_options.pop( + "new_session_name", original_options.get("new_session_name") + ) + + if last_idx > 0 and idx < last_idx: + detached = True + new_session_name = None + + load_workspace( + config_file, + detached=detached, + new_session_name=new_session_name, + **tmux_options, ) From f31902fdd552715dbaf280eeeaf101c4fc473678 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Oct 2022 08:07:46 -0500 Subject: [PATCH 5/7] tests(cli[load]): Test loading of multiple workspaces --- tests/test_cli.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index c0b41d84332..f5baed95d02 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -441,7 +441,7 @@ def test_load_symlinked_workspace( class CLILoadFixture(t.NamedTuple): test_id: str - cli_args: t.List[str] + cli_args: t.List[t.Union[str, t.List[str]]] config_paths: t.List[str] expected_exit_code: int expected_in_out: "ExpectedOutput" = None @@ -499,6 +499,20 @@ class CLILoadFixture(t.NamedTuple): expected_in_out=None, expected_not_in_out=None, ), + # + # Multiple configs + # + CLILoadFixture( + test_id="configdir-session-name-double", + cli_args=["load", "my_config", "second_config"], + config_paths=[ + "{TMUXP_CONFIGDIR}/my_config.yaml", + "{TMUXP_CONFIGDIR}/second_config.yaml", + ], + expected_exit_code=0, + expected_in_out=None, + expected_not_in_out=None, + ), ] @@ -533,17 +547,19 @@ def test_load( ) tmuxp_config.write_text( """ - session_name: test + session_name: {session_name} windows: - window_name: test panes: - - """, + """.format( + session_name=pathlib.Path(config_path).name.replace(".", "") + ), encoding="utf-8", ) try: - cli.cli([*cli_args, "-d", "-L", server.socket_name]) + cli.cli([*cli_args, "-d", "-L", server.socket_name, "-y"]) except SystemExit: pass From dfefd0a3480ca0401fa4fc1456b03d52d6718089 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Oct 2022 08:10:28 -0500 Subject: [PATCH 6/7] tests(cli[load]): Assert session_name exists --- tests/test_cli.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index f5baed95d02..0fd1f381a87 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -443,6 +443,7 @@ class CLILoadFixture(t.NamedTuple): test_id: str cli_args: t.List[t.Union[str, t.List[str]]] config_paths: t.List[str] + session_names: t.List[str] expected_exit_code: int expected_in_out: "ExpectedOutput" = None expected_not_in_out: "ExpectedOutput" = None @@ -455,6 +456,7 @@ class CLILoadFixture(t.NamedTuple): test_id="dir-relative-dot-samedir", cli_args=["load", "."], config_paths=["{tmp_path}/.tmuxp.yaml"], + session_names=["my_config"], expected_exit_code=0, expected_in_out=None, expected_not_in_out=None, @@ -463,6 +465,7 @@ class CLILoadFixture(t.NamedTuple): test_id="dir-relative-dot-slash-samedir", cli_args=["load", "./"], config_paths=["{tmp_path}/.tmuxp.yaml"], + session_names=["my_config"], expected_exit_code=0, expected_in_out=None, expected_not_in_out=None, @@ -471,6 +474,7 @@ class CLILoadFixture(t.NamedTuple): test_id="dir-relative-file-samedir", cli_args=["load", "./.tmuxp.yaml"], config_paths=["{tmp_path}/.tmuxp.yaml"], + session_names=["my_config"], expected_exit_code=0, expected_in_out=None, expected_not_in_out=None, @@ -479,6 +483,7 @@ class CLILoadFixture(t.NamedTuple): test_id="filename-relative-file-samedir", cli_args=["load", "./my_config.yaml"], config_paths=["{tmp_path}/my_config.yaml"], + session_names=["my_config"], expected_exit_code=0, expected_in_out=None, expected_not_in_out=None, @@ -487,6 +492,7 @@ class CLILoadFixture(t.NamedTuple): test_id="configdir-session-name", cli_args=["load", "my_config"], config_paths=["{TMUXP_CONFIGDIR}/my_config.yaml"], + session_names=["my_config"], expected_exit_code=0, expected_in_out=None, expected_not_in_out=None, @@ -495,6 +501,7 @@ class CLILoadFixture(t.NamedTuple): test_id="configdir-absolute", cli_args=["load", "~/.config/tmuxp/my_config.yaml"], config_paths=["{TMUXP_CONFIGDIR}/my_config.yaml"], + session_names=["my_config"], expected_exit_code=0, expected_in_out=None, expected_not_in_out=None, @@ -509,6 +516,7 @@ class CLILoadFixture(t.NamedTuple): "{TMUXP_CONFIGDIR}/my_config.yaml", "{TMUXP_CONFIGDIR}/second_config.yaml", ], + session_names=["my_config", "second_config"], expected_exit_code=0, expected_in_out=None, expected_not_in_out=None, @@ -532,6 +540,7 @@ def test_load( test_id: str, cli_args: t.List[str], config_paths: t.List[str], + session_names: t.List[str], expected_exit_code: int, expected_in_out: "ExpectedOutput", expected_not_in_out: "ExpectedOutput", @@ -541,7 +550,7 @@ def test_load( assert server.socket_name is not None monkeypatch.chdir(tmp_path) - for config_path in config_paths: + for session_name, config_path in zip(session_names, config_paths): tmuxp_config = pathlib.Path( config_path.format(tmp_path=tmp_path, TMUXP_CONFIGDIR=tmuxp_configdir) ) @@ -553,7 +562,7 @@ def test_load( panes: - """.format( - session_name=pathlib.Path(config_path).name.replace(".", "") + session_name=session_name ), encoding="utf-8", ) @@ -578,6 +587,9 @@ def test_load( for needle in expected_not_in_out: assert needle not in output + for session_name in session_names: + assert server.has_session(session_name) + def test_regression_00132_session_name_with_dots( tmp_path: pathlib.Path, From fff6f4f82eeb342e18dc21213a484db3f9f131a5 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 29 Oct 2022 08:59:13 -0500 Subject: [PATCH 7/7] chore(cli[load]): Simplify --- src/tmuxp/cli/load.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/tmuxp/cli/load.py b/src/tmuxp/cli/load.py index 85f19dfe896..7ae88210f53 100644 --- a/src/tmuxp/cli/load.py +++ b/src/tmuxp/cli/load.py @@ -637,15 +637,14 @@ def command_load( return last_idx = len(args.config_files) - 1 - original_options = tmux_options.copy() + original_detached_option = tmux_options.pop("detached") + original_new_session_name = tmux_options.pop("new_session_name") for idx, config_file in enumerate(args.config_files): config_file = scan_config(config_file, config_dir=get_config_dir()) - detached = tmux_options.pop("detached", original_options.get("detached", False)) - new_session_name = tmux_options.pop( - "new_session_name", original_options.get("new_session_name") - ) + detached = original_detached_option + new_session_name = original_new_session_name if last_idx > 0 and idx < last_idx: detached = True