From 457029080d17e3288bf3b483ff055c32af68bf2c Mon Sep 17 00:00:00 2001 From: Gerard Roche Date: Sat, 20 Jan 2024 22:00:26 +0000 Subject: [PATCH] feat: tmux strategy to run tests in a tmux pane See [README.md](README.md) for usage. Close #119 **Tmux Settings** :new: Configure Tmux settings for running tests in a tmux pane: | Setting | Type | Default | Description | :-------------------------------- | :------------ | :-------- | :---------- | `phpunit.tmux_clear` | `bool` | `true` | Clear the terminal screen before running tests. | `phpunit.tmux_clear_scrollback` | `bool` | `false` | Clear the terminal's scrollback buffer using the extended "E3" capability. | `phpunit.tmux_target` | `string` | `:.` | Specify the session, window, and pane which should be used to run tests.

Format: `{session}:{window}.{pane}`

The default means the current pane.

For example, `:{start}.{top}` would mean the current session, lowest-numbered window, top pane.

See [Tmux documentation](http://man.openbsd.org/OpenBSD-current/man1/tmux.1#COMMANDS) for target usage. **Example:** Run tests in a Tmux pane. ```json { "phpunit.strategy": "tmux", "phpunit.options": { "colors": true, "no-coverage": true } } ``` Tip: Use the **`no-coverage`** option with the Command Palette **PHPUnit: Toggle --no-coverage** to turn code coverage on and off for quicker test runs when you just don't need the code coverage report. **Example:** Run tests in current session, lowest-numbered window, and top pane ```json { "phpunit.strategy": "tmux", "phpunit.tmux_target": ":{start}.{top}", } ``` The target accepts the format `{target-session}:{target-window}.{target-pane}`. The default is `:.`, which means the current pane. See [Tmux documentation](http://man.openbsd.org/OpenBSD-current/man1/tmux.1#COMMANDS) for more details on the target usage. --- CHANGELOG.md | 6 +++ Preferences.sublime-settings | 20 +++++++++- README.md | 62 +++++++++++++++++++++-------- lib/runner.py | 11 ++---- lib/strategy.py | 31 ++++++++++++++- lib/utils.py | 76 ++++++++++++++++++++++++++++++++++++ 6 files changed, 181 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b479c5c..f7837de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. +## 3.19.0 + +### Added + +- Tmux strategy - Runs test commands in a tmux pane [#119](https://github.com/gerardroche/sublime-phpunit/issues/119) + ## 3.18.3 - Unreleased ### Fixed diff --git a/Preferences.sublime-settings b/Preferences.sublime-settings index 2bcc8c1..de804a5 100644 --- a/Preferences.sublime-settings +++ b/Preferences.sublime-settings @@ -94,5 +94,23 @@ // paths and the values are the replacement remote paths. Environment // variables and user home directory ~ placeholder are expanded. // Example: {"~/code/project1": "~/project1"} - "phpunit.docker_paths": {} + "phpunit.docker_paths": {}, + + // Clear the terminal screen before running tests. + "phpunit.tmux_clear": true, + + // Clear the terminal's scrollback buffer using the extended "E3" capability. + "phpunit.tmux_clear_scrollback": true, + + // Specify the session, window, and pane which should be used to run tests. + // + // Format: `{session}:{window}.{pane}` + // + // The default means the current pane. + // + // For example, ":{start}.{top}", would mean the current session, + // lowest-numbered window, top pane. + // + // See http://man.openbsd.org/OpenBSD-current/man1/tmux.1#COMMANDS + "phpunit.tmux_target": ":." } diff --git a/README.md b/README.md index 759e073..564d559 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Enhance your coding experience with seamless PHPUnit integration for [Sublime Te - [xterm] - A terminal emulator for the X Window System. - [cmd] - A command-line interpreter for Windows. - [PowerShell] - A cross-platform command-line shell. + - [Tmux] - A terminal multiplexer. :new: * Zero configuration required Read [Running PHPUnit Tests from Sublime Text](https://blog.gerardroche.com/2023/05/05/running-phpunit-tests-within-sublime-text/) for a quick introduction. @@ -58,6 +59,7 @@ Read [Running PHPUnit Tests from Sublime Text](https://blog.gerardroche.com/2023 - [PHP Executable](#php-executable) - [SSH](#ssh) - [Docker](#docker) + - [Tmux](#tmux) - [Auto Commands](#auto-commands) - [Toggle Commands](#toggle-commands) - [Custom Toggle Commands](#custom-toggle-commands) @@ -150,17 +152,13 @@ Enhance your testing workflow with these commands for efficient testing directly PHPUnitKit can run tests using different execution environments known as "strategies". -**Example:** Using the Kitty Terminal Strategy - -To set this strategy: +**Example:** Using the Tmux Strategy 1. Open the Command Palette: `Preferences: PHPUnit Settings` 2. Add the following to your settings: -```json -{ - "phpunit.strategy": "kitty" -} +``` +"phpunit.strategy": "tmux" ``` Available strategies and their identifiers: @@ -173,6 +171,7 @@ Available strategies and their identifiers: | **[xterm]** | `xterm` | Sends test commands to the xterm terminal. | **[cmd]** | `cmd` | Sends test commands to the cmd.exe terminal. | **[PowerShell]** | `powershell` | Sends test commands to the PowerShell command shell. +| **[Tmux]** | `tmux` | Sends test commands to the Tmux terminal multiplexer. :new: ## Configuration @@ -184,9 +183,9 @@ Available settings and their details: | Setting | Type | Default | Description | :------------------------ | :----------------- | :------------------- | :---------- -| `phpunit.executable` | `string` or `list` | Auto-discovery | Path to the PHPUnit executable for running tests. Environment variables and user home directory ~ placeholder are expanded. The executable can be a string or a list of parameters. Example: `vendor/bin/phpunit` +| `phpunit.executable` | `string` or `list` | Auto-discovery | Path to the PHPUnit executable for running tests. Environment variables and user home directory ~ placeholder are expanded. The executable can be a string or a list of parameters. Example: `vendor/bin/phpunit` | `phpunit.options` | `dict` | `{}` | Command-line options to pass to PHPUnit. Example: `{"no-coverage": true}` -| `phpunit.php_executable` | `string` | Auto-discovery | Path to the PHP executable for running tests. Environment variables and user home directory ~ placeholder are expanded. Example: `~/.phpenv/versions/8.2/bin/php` +| `phpunit.php_executable` | `string` | Auto-discovery | Path to the PHP executable for running tests. Environment variables and user home directory ~ placeholder are expanded. Example: `~/.phpenv/versions/8.2/bin/php` | `phpunit.save_all_on_run` | `boolean` | `true` | Automatically saves all unsaved buffers before running tests. | `phpunit.on_post_save` | `list` | `[]` | Auto commands to execute when views are saved. Example: `["phpunit_test_file"]` | `phpunit.debug` | `boolean` | `false` | Prints debug information about the test runner. @@ -198,9 +197,7 @@ Available settings and their details: | `phpunit.paratest` | `boolean` | `false` | Uses ParaTest to run tests. | `phpunit.pest` | `boolean` | `false` | Uses Pest to run tests. -These settings allow you to customize PHPUnitKit according to your preferences and requirements. - -**SSH Settings** :rocket: +**SSH Settings** Configure SSH settings for running tests remotely: @@ -212,9 +209,7 @@ Configure SSH settings for running tests remotely: | `phpunit.ssh_host` | `string` | `null` | Host for running tests via SSH. Example: `homestead.test` | `phpunit.ssh_paths` | `dict` | `{}` | Path mapping for running tests via SSH. Keys: local paths, Values: corresponding remote paths. Environment variables and user home directory ~ placeholder are expanded. Example: `{"~/code/project1": "~/project1"}` -Use these settings to configure PHPUnitKit's SSH options for seamless remote testing. - -**Docker Settings** :rocket: +**Docker Settings** Configure Docker settings for running tests within containers: @@ -224,7 +219,15 @@ Configure Docker settings for running tests within containers: | `phpunit.docker_command` | `list` | `[]` | Command to use when running tests via Docker. Example: `["docker", "exec", "-it", "my-container"]` | `phpunit.docker_paths` | `dict` | `{}` | Path mapping for running tests via Docker. Keys: local paths, Values: corresponding remote paths. Environment variables and user home directory ~ placeholder are expanded. Example: `{"~/code/project1": "~/project1"}` -Utilize these settings to configure PHPUnitKit for streamlined testing within Docker containers. +**Tmux Settings** :new: + +Configure Tmux settings for running tests in a tmux pane: + +| Setting | Type | Default | Description +| :-------------------------------- | :------------ | :-------- | :---------- +| `phpunit.tmux_clear` | `bool` | `true` | Clear the terminal screen before running tests. +| `phpunit.tmux_clear_scrollback` | `bool` | `false` | Clear the terminal's scrollback buffer using the extended "E3" capability. +| `phpunit.tmux_target` | `string` | `:.` | Specify the session, window, and pane which should be used to run tests.

Format: `{session}:{window}.{pane}`

The default means the current pane.

For example, `:{start}.{top}` would mean the current session, lowest-numbered window, top pane.

See [Tmux documentation](http://man.openbsd.org/OpenBSD-current/man1/tmux.1#COMMANDS) for target usage. ### CLI Options @@ -367,6 +370,32 @@ Command Palette → Preferences: PHPUnit Settings } ``` +### Tmux :new: + +**Example:** Run tests in a Tmux pane. + +```json +{ + "phpunit.strategy": "tmux", + "phpunit.options": { "colors": true, "no-coverage": true } +} +``` + +Tip: Use the **`no-coverage`** option with the Command Palette **PHPUnit: Toggle --no-coverage** to turn code coverage on and off for quicker test runs when you just don't need the code coverage report. + +**Example:** Run tests in current session, lowest-numbered window, and top pane + +```json +{ + "phpunit.strategy": "tmux", + "phpunit.tmux_target": ":{start}.{top}" +} +``` + +The target accepts the format `{target-session}:{target-window}.{target-pane}`. The default is `:.`, which means the current pane. + +See [Tmux documentation](http://man.openbsd.org/OpenBSD-current/man1/tmux.1#COMMANDS) for more details on the target usage. + ### Auto Commands You can configure the `on_post_save` event to run the "Test File" command when views are saved. This will instruct the runner to automatically run a test every time it is saved. @@ -488,6 +517,7 @@ Released under the [GPL-3.0-or-later License](LICENSE). [ParaTest]: https://github.com/paratestphp/paratest [Pest]: https://pestphp.com [PowerShell]: https://learn.microsoft.com/en-us/powershell/ +[Tmux]: https://github.com/tmux/tmux/wiki [cmd]: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd [iTerm2]: https://iterm2.com [xterm]: https://invisible-island.net/xterm/ diff --git a/lib/runner.py b/lib/runner.py index da57633..722a147 100644 --- a/lib/runner.py +++ b/lib/runner.py @@ -138,13 +138,10 @@ def run(self, working_dir=None, file=None, options=None) -> None: 'options': options }) - strategy.execute( - self.window, - self.view, - env, - cmd, - working_dir - ) + if get_setting(self.view, 'strategy') == 'tmux': + cmd = strategy.build_tmux_cmd(self.view, working_dir, cmd) + + strategy.execute(self.window, self.view, env, cmd, working_dir) def run_last(self) -> None: last_test_args = get_last_run() diff --git a/lib/strategy.py b/lib/strategy.py index 26ad8e0..ab1afc0 100644 --- a/lib/strategy.py +++ b/lib/strategy.py @@ -17,6 +17,7 @@ import os import re +import shlex from sublime import cache_path from sublime import load_resource @@ -28,7 +29,7 @@ def execute(window, view, env: dict, cmd: list, working_dir: str) -> None: - if get_setting(view, 'strategy') in ('cmd', 'external', 'iterm', 'kitty', 'powershell', 'xterm'): + if get_setting(view, 'strategy') in ('cmd', 'external', 'iterm', 'kitty', 'powershell', 'tmux', 'xterm'): window.run_command('exec', { 'env': env, 'cmd': cmd, @@ -138,3 +139,31 @@ def _get_color_scheme(view): ' scheme with PHPUnit test results colors: {}'.format(str(e))) return color_scheme + + +def build_tmux_cmd(view, working_dir: str, cmd: list) -> list: + tmux_target = get_setting(view, 'tmux_target') + + # Try make initial cmd relative to working directory to reduce length. + if cmd[0].startswith(working_dir): + cmd = [os.path.relpath(cmd[0], working_dir)] + cmd[1:] + + key_cmds = [] + + # Clear the terminal screen. + if get_setting(view, 'tmux_clear'): + clear_cmd = ['clear'] + if get_setting(view, 'tmux_clear_scrollback'): + clear_cmd.append('-x') + key_cmds.append(shlex.join(clear_cmd)) + + # Switch to the working directory. + key_cmds.append(shlex.join(['cd', working_dir])) + + # The test command. + key_cmds.append(shlex.join(cmd)) + + # Run inside a subshell to avoid changing the current working directory. + keys = '({})\n'.format(' && '.join(key_cmds)) + + return ['tmux', 'send-keys', '-t', tmux_target, keys] diff --git a/lib/utils.py b/lib/utils.py index 9ae7733..bfae24e 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -18,6 +18,9 @@ import os import re import shutil +import subprocess +import sys +import traceback from sublime import active_window from sublime import platform @@ -750,3 +753,76 @@ def toggle_on_post_save(view, item: str) -> None: view.settings().erase('phpunit.on_post_save') if on_post_save != view.settings().get('phpunit.on_post_save'): view.settings().set('phpunit.on_post_save', on_post_save) + + +def _get_default_shell() -> str: + if sys.platform.startswith('linux') or sys.platform.startswith('darwin'): + return os.environ.get('SHELL', 'sh') + elif sys.platform.startswith('win'): + return 'cmd.exe' + else: + return '' + + +if sys.platform.startswith('win'): + try: + import ctypes + except ImportError: + traceback.print_exc() + ctypes = None + + def _get_startup_info(): + # Hide the child process window. + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + return startupinfo + + def _get_encoding() -> str: + return str(ctypes.windll.kernel32.GetOEMCP()) + + def _shell_filter_newlines(text: str): + return text.replace('\r\n', '\n') + + def _shell_decode(res): + return _shell_filter_newlines(res.decode(_get_encoding())) + + def _shell_read(view, cmd: str) -> str: + p = subprocess.Popen([_get_default_shell(), '/c', cmd], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + startupinfo=_get_startup_info()) + out, err = p.communicate() + + if out: + return _shell_decode(out) + + if err: + return _shell_decode(err) + + return '' + +else: + def _shell_read(view, cmd: str) -> str: + p = subprocess.Popen([_get_default_shell(), '-c', cmd], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + out, err = p.communicate() + + if out: + return out.decode('utf-8').strip() + + if err: + return err.decode('utf-8').strip() + + return '' + + +def shell_read(view, cmd: str) -> str: + try: + return _shell_read(view, cmd) + except Exception: + traceback.print_exc() + + return ''