diff --git a/src/internal/config_function.go b/src/internal/config_function.go index 8d44b956..3a6417c8 100644 --- a/src/internal/config_function.go +++ b/src/internal/config_function.go @@ -41,8 +41,7 @@ func initialConfig(dir string) (toggleDotFileBool bool, toggleFooter bool, first slog.SetDefault(slog.New(slog.NewTextHandler( file, &slog.HandlerOptions{Level: logLevel}))) - slog.Debug("Runtime information", "runtime.GOOS", runtime.GOOS) - + loadHotkeysFile() loadThemeFile() @@ -93,6 +92,9 @@ func initialConfig(dir string) (toggleDotFileBool bool, toggleFooter bool, first firstFilePanelDir = variable.HomeDir } + slog.Debug("Runtime information", "runtime.GOOS", runtime.GOOS, + "start directory", firstFilePanelDir) + return toggleDotFileBool, toggleFooter, firstFilePanelDir } diff --git a/src/internal/model.go b/src/internal/model.go index bdf04ba8..ec8503bb 100644 --- a/src/internal/model.go +++ b/src/internal/model.go @@ -462,4 +462,5 @@ func (m *model) quitSuperfile() { currentDir = strings.ReplaceAll(currentDir, "'", "'\\''") os.WriteFile(variable.SuperFileStateDir+"/lastdir", []byte("cd '"+currentDir+"'"), 0755) } + slog.Debug("Quitting superfile", "current dir", currentDir) } diff --git a/testsuite/.gitignore b/testsuite/.gitignore new file mode 100644 index 00000000..15f85227 --- /dev/null +++ b/testsuite/.gitignore @@ -0,0 +1,9 @@ +# python venv site packages +site-packages/ + +#python venvs +.venv/ + +# python pycache +__pycache__/ +*.pyc \ No newline at end of file diff --git a/testsuite/Notes.md b/testsuite/Notes.md new file mode 100644 index 00000000..c0eda671 --- /dev/null +++ b/testsuite/Notes.md @@ -0,0 +1,68 @@ +# Implementation notes + +- The `pyautogui` sends input to the process in focus, which is the `spf` subprocess. +- If `spf` is not exited correcly via `q`, it causes wierd vertical tabs in print statements from python +- There is some flakiness in sending of input. Many times, `Ctrl+C` is received as `C` in `spf` + - If first key is `Ctrl+C`, its always received as `C` +- Note : You must keep your focus on the terminal for the entire duration of test run. `pyautogui` sends keypress to process on focus. + +## Input to spf + +### Pyautogui alternatives +POC with pyautogui as a lot of issues, stated above. + +#### Linux / MacOS + +- xdotool + - Seems complicated. It wont be able to manage spf process that well +- mkfifo / Manual linux piping + - Too much manual work to send inputs, even if it works +- tmux + - Supports full terminal programs and has a python wrapper library + - See `docs/tmux.md` + - Not available for windows +- References + - https://superuser.com/questions/585398/sending-simulated-keystrokes-in-bash + +#### Windows + +- Autohotkey + - No better than pyautogui +- ControlSend and SendInput utility in windows + - Isn't that just for C# / C++ code ? +- Python ctypes + - https://stackoverflow.com/questions/62189991/how-to-wrap-the-sendinput-function-to-python-using-ctypes +- pywin32 library + - Create a new GUI window for test + - Use `win32gui.SendMessage` or `win32gui.PostMessage` + - Probably the correct way, but I havent been able to get it working. + - First we need to get it send input to a sample window like notepad, etc. Then we can make superfile work +- pywinpty + - Heavy installations requirements. Needs Rust, and Visual studio build tools. + - Rust cargo not found + - Needs rust + - link.exe not found (` the msvc targets depend on the msvc linker but link.exe was not found` ) + - Needs to install Visual Studio Build Tools (build tools and spectre mitigated libs) + - Had to manually find link.exe and put it on the PATH + - You might get error of unable to find mspdbcore.dll (I havent been able to solve it so far) + - https://stackoverflow.com/questions/67328795/c1356-unable-to-find-mspdbcore-dll +- References + - https://www.reddit.com/r/tmux/comments/l580mi/is_there_a_tmuxlike_equivalent_for_windows/ + +## Directory setup +- Programmatic setup is better. +- We could keep test directory setup as a config file - json/yaml/toml +- or as a hardcoded python dict +- Turns out, a in-memory fs is better. We have utilities like copy to actual fs and print tree + - Although it has a limitation of not being able to work with large files, as that would consume a lot of RAM + - For large files, we could do actually make them only on the actual filesystem, and not use in-memory fs + - https://docs.pyfilesystem.org/en/latest/reference/memoryfs.html + - https://docs.pyfilesystem.org/en/latest/guide.html + + +## Tests and Validation +- Each tests starts independently, so there is no strict order +- Hardcoded validations . Predefined test, where each test has start dir, key press, and validations +- We could have a base Class test. where check(), input(), init(), methods would be overrided +- It allows greater flexibility in terms of testcases. +- Abstraction layer for spf init, teardown and inputm \ No newline at end of file diff --git a/testsuite/ReadMe.md b/testsuite/ReadMe.md new file mode 100644 index 00000000..314c10fe --- /dev/null +++ b/testsuite/ReadMe.md @@ -0,0 +1,78 @@ +## Coding style rules +- Prefer using strong typing +- Prefer using type hinting for the first time the variable is declared, and for functions paremeters and return types +- Use `-> None` to explicitly indicate no return value + +### Ideas +- Recommended to integrate your IDE with PEP8 to highlight PEP8 violations in real-time +- Enforcing PEP8 via `pylint flake8 pycodestyle` and via pre commit hooks + +## Writing New testcases +- Just create a file ending with `_test.py` in `tests` directory + - Any subclass of BaseTest with name ending with `Test` will be executed + - see `run_tests` and `get_testcases` in `core/runner.py` for more info + +## Setup +Requires python 3.9 or later. + +## Setup for MacOS / Linux + +### Install tmux +- You need to have tmux installed. See https://github.com/tmux/tmux/wiki + +### Python virtual env setup +``` +# cd to this directory +cd +python3 -m venv .venv +.venv/bin/pip install --upgrade pip +.venv/bin/pip install -r requirements.txt +``` + +### Make sure you build spf +``` +# cd to the superfile repo root (parent of this) +cd +./build.sh +``` + +### Running testsuite +``` +.venv/bin/python3 main.py +``` +## Setup for Windows +Coming soon. + + + +### Python virtual env setup +``` +# cd to this directory +cd +python3 -m venv .venv +.venv\Scripts\python -m pip install --upgrade pip +.venv\Scripts\pip -r requirements.txt +``` + +### Make sure you build spf +``` +# cd to the superfile repo root (parent of this) +cd +go build -o bin/spf.exe +``` + +### Running testsuite +Notes +- You must keep your focus on the terminal for the entire duration of test run. `pyautogui` sends keypress to process on focus. + +``` +.venv\Scripts\python main.py +``` + +## Tips while running tests +- Use `-d` or `--debug` to enable debug logs during test run. +- If you see flakiness in test runs due to superfile being still open, consider using `--close-wait-time` options to increase wait time for superfile to close +- Use `-t` or `--tests` to only run specific tests + - Example `python main.py -d -t RenameTest CopyTest` + +- If you see `libtmux` errors like `libtmux.exc.LibTmuxException: ['no server running on /private/tmp/tmux-501/superfile']` Make sure your python version is up to date \ No newline at end of file diff --git a/testsuite/core/__init__.py b/testsuite/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testsuite/core/base_test.py b/testsuite/core/base_test.py new file mode 100644 index 00000000..a79ad4ba --- /dev/null +++ b/testsuite/core/base_test.py @@ -0,0 +1,108 @@ +import logging +import time +from abc import ABC, abstractmethod +from core.environment import Environment +from pathlib import Path +from typing import Union, List, Tuple +import core.keys as keys +import core.test_constants as tconst + + +class BaseTest(ABC): + """Base class for all tests + The idea is to have independency among each test. + And for each test to have full control on its environment, execution, and validation. + """ + def __init__(self, test_env : Environment): + self.env = test_env + self.logger = logging.getLogger() + + @abstractmethod + def setup(self) -> None: + """Set up the required things for test + """ + + @abstractmethod + def test_execute(self) -> None: + """Execute the test + """ + + @abstractmethod + def validate(self) -> bool: + """Validate that test passed. Log exception if failed. + Returns: + bool: True if validation passed + """ + +class GenericTestImpl(BaseTest): + def __init__(self, test_env : Environment, + test_root : Path, + start_dir : Path, + test_dirs : List[Path], + test_files : List[Tuple[Path, str]], + key_inputs : List[Union[keys.Keys,str]], + validate_exists : List[Path] = [], + validate_not_exists : List[Path] = []): + super().__init__(test_env) + self.test_root = test_root + self.start_dir = start_dir + self.test_dirs = test_dirs + self.test_files = test_files + self.key_inputs = key_inputs + self.validate_exists = validate_exists + self.validate_not_exists = validate_not_exists + + def setup(self) -> None: + for dir_path in self.test_dirs: + self.env.fs_mgr.makedirs(dir_path) + for file_path, data in self.test_files: + self.env.fs_mgr.create_file(file_path, data) + + self.logger.debug("Current file structure : \n%s", + self.env.fs_mgr.tree(self.test_root)) + + + def test_execute(self) -> None: + """Execute the test + """ + # Start in DIR1 + self.env.spf_mgr.start_spf(self.env.fs_mgr.abspath(self.start_dir)) + + assert self.env.spf_mgr.is_spf_running(), "Superfile is not running" + + for cur_input in self.key_inputs: + if isinstance(cur_input, keys.Keys): + self.env.spf_mgr.send_special_input(cur_input) + else: + assert isinstance(cur_input, str), "Invalid input type" + self.env.spf_mgr.send_text_input(cur_input) + time.sleep(tconst.KEY_DELAY) + + time.sleep(tconst.OPERATION_DELAY) + self.env.spf_mgr.send_special_input(keys.KEY_ESC) + time.sleep(tconst.CLOSE_WAIT_TIME) + self.logger.debug("Finished Execution") + + def validate(self) -> bool: + """Validate that test passed. Log exception if failed. + Returns: + bool: True if validation passed + """ + self.logger.debug("spf_manager info : %s, Current file structure : \n%s", + self.env.spf_mgr.runtime_info(), self.env.fs_mgr.tree(self.test_root)) + try: + assert not self.env.spf_mgr.is_spf_running(), "Superfile is still running" + for file_path in self.validate_exists: + assert self.env.fs_mgr.check_exists(file_path), f"File {file_path} does not exists" + + for file_path in self.validate_not_exists: + assert not self.env.fs_mgr.check_exists(file_path), f"File {file_path} exists" + except AssertionError as ae: + self.logger.debug("Test assertion failed : %s", ae, exc_info=True) + return False + + return True + + def __repr__(self) -> str: + return f"{self.__class__.__name__}" + diff --git a/testsuite/core/environment.py b/testsuite/core/environment.py new file mode 100644 index 00000000..05637f39 --- /dev/null +++ b/testsuite/core/environment.py @@ -0,0 +1,14 @@ +from core.spf_manager import BaseSPFManager +from core.fs_manager import TestFSManager + +class Environment: + """Manage test environment + Manage cleanup of environment and other stuff at a single place + """ + def __init__(self, spf_manager : BaseSPFManager, fs_manager : TestFSManager ): + self.spf_mgr = spf_manager + self.fs_mgr = fs_manager + + def cleanup(self) -> None: + self.spf_mgr.close_spf() + self.fs_mgr.cleanup() \ No newline at end of file diff --git a/testsuite/core/fs_manager.py b/testsuite/core/fs_manager.py new file mode 100644 index 00000000..7f9841de --- /dev/null +++ b/testsuite/core/fs_manager.py @@ -0,0 +1,56 @@ +import logging +from tempfile import TemporaryDirectory +from pathlib import Path +import os +from io import StringIO + +class TestFSManager: + """Manage the temporary files for test and the cleanup + """ + def __init__(self): + self.logger = logging.getLogger() + self.logger.debug("Initialized %s", self.__class__.__name__) + self.temp_dir_obj = TemporaryDirectory() + self.temp_dir = Path(self.temp_dir_obj.name) + + def abspath(self, relative_path : Path) -> Path: + return self.temp_dir / relative_path + + def check_exists(self, relative_path : Path) -> bool: + return self.abspath(relative_path).exists() + + def makedirs(self, relative_path : Path) -> None: + # Overloaded '/' operator + os.makedirs(self.temp_dir / relative_path, exist_ok=True) + + def create_file(self, relative_path : Path, data : str = "") -> None: + """Create files + Make sure directories exist + Args: + relative_path (Path): Relative path from test root + """ + with open(self.temp_dir / relative_path, 'w', encoding="utf-8") as f: + f.write(data) + + def tree(self, relative_root : Path = None) -> str: + if relative_root is None: + root = self.temp_dir + else: + root = self.temp_dir / relative_root + res = StringIO() + for item in root.rglob('*'): + path_str = str(item.relative_to(root)) + if item.is_dir(): + res.write(f"D-{path_str}\n") + else: + res.write(f"F-{path_str}\n") + return res.getvalue() + + def cleanup(self) -> None: + """Cleaup the temporary directory + Its okay to forget it though, it will be cleaned on program exit then. + """ + self.temp_dir_obj.cleanup() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(temp_dir = {self.temp_dir})" diff --git a/testsuite/core/keys.py b/testsuite/core/keys.py new file mode 100644 index 00000000..6689a04d --- /dev/null +++ b/testsuite/core/keys.py @@ -0,0 +1,52 @@ +from abc import ABC + +class Keys(ABC): + def __init__(self, ascii_code : int): + self.ascii_code = ascii_code + + def __repr__(self) -> str: + return f"Key(code={self.ascii_code})" + +# Will isinstance of Keys work for object of CtrlKeys ? +class CtrlKeys(Keys): + def __init__(self, char : str): + # Only allowing single alphabetic character + # assert is good here as all objects are defined statically + assert len(char) == 1 + assert char.isalpha() and char.islower() + self.char = char + # Ctrl + A starts at 1 + super().__init__(ord(char) - ord('a') + 1) + +# Maybe have keycode +class SpecialKeys(Keys): + def __init__(self, ascii_code : int, key_name : str): + super().__init__(ascii_code) + self.key_name = key_name + + + +KEY_CTRL_A : Keys = CtrlKeys('a') +KEY_CTRL_C : Keys = CtrlKeys('c') +KEY_CTRL_D : Keys = CtrlKeys('d') +KEY_CTRL_M : Keys = CtrlKeys('m') +KEY_CTRL_R : Keys = CtrlKeys('r') +KEY_CTRL_V : Keys = CtrlKeys('v') +KEY_CTRL_X : Keys = CtrlKeys('x') + +# See https://vimdoc.sourceforge.net/htmldoc/digraph.html#digraph-table for key codes +# If keyname is not the same string as key code in pyautogui, need to handle seperately +KEY_BACKSPACE : Keys = SpecialKeys(8 , "Backspace") +KEY_ENTER : Keys = SpecialKeys(13, "Enter") +KEY_ESC : Keys = SpecialKeys(27, "Esc") +KEY_DELETE : Keys = SpecialKeys(127 , "Delete") + + +NO_ASCII = -1 + +# Some keys dont have ascii codes, they have to be handled separately +# Make sure key name is the same string as key code for Tmux +KEY_DOWN : Keys = SpecialKeys(NO_ASCII, "Down") +KEY_UP : Keys = SpecialKeys(NO_ASCII, "Up") +KEY_LEFT : Keys = SpecialKeys(NO_ASCII, "Left") +KEY_RIGHT : Keys = SpecialKeys(NO_ASCII, "Right") diff --git a/testsuite/core/pyautogui_manager.py b/testsuite/core/pyautogui_manager.py new file mode 100644 index 00000000..68bb778b --- /dev/null +++ b/testsuite/core/pyautogui_manager.py @@ -0,0 +1,62 @@ +import time +import subprocess +import pyautogui +import core.keys as keys +from core.spf_manager import BaseSPFManager + +class PyAutoGuiSPFManager(BaseSPFManager): + """Manage SPF via subprocesses and pyautogui + Cross platform, but it globally takes over the input, so you need the terminal + constantly on focus during test run + """ + SPF_START_DELAY : float = 0.5 + def __init__(self, spf_path : str): + super().__init__(spf_path) + self.spf_process = None + + + def start_spf(self, start_dir : str = None) -> None: + self.spf_process = subprocess.Popen([self.spf_path, start_dir], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + time.sleep(PyAutoGuiSPFManager.SPF_START_DELAY) + + # Need to send a sample keypress otherwise it ignores first keypress + self.send_text_input('x') + + + def send_text_input(self, text : str, all_at_once : bool = False) -> None: + if all_at_once : + pyautogui.write(text) + else: + for c in text: + pyautogui.write(c) + + def send_special_input(self, key : keys.Keys) -> None: + if isinstance(key, keys.CtrlKeys): + pyautogui.hotkey('ctrl', key.char) + elif isinstance(key, keys.SpecialKeys): + pyautogui.press(key.key_name.lower()) + else: + raise Exception(f"Unknown key : {key}") + + def get_rendered_output(self) -> str: + return "[Not supported yet]" + + + def is_spf_running(self) -> bool: + self._is_spf_running = (self.spf_process is not None) and (self.spf_process.poll() is None) + return self._is_spf_running + + def close_spf(self) -> None: + if self.spf_process is not None: + self.spf_process.terminate() + + # Override + def runtime_info(self) -> str: + if self.spf_process is None: + return "[No process]" + else: + return f"[PID : {self.spf_process.pid}, poll : {self.spf_process.poll()}]" + + + diff --git a/testsuite/core/runner.py b/testsuite/core/runner.py new file mode 100644 index 00000000..5d4bbd1f --- /dev/null +++ b/testsuite/core/runner.py @@ -0,0 +1,79 @@ +from core.spf_manager import BaseSPFManager +from core.fs_manager import TestFSManager +from core.environment import Environment +from core.base_test import BaseTest + +import logging +import platform +import importlib +from pathlib import Path +from typing import List + + +# Preferred importing at the top level +if platform.system() == "Windows" : + # Conditional import is needed to make it work on linux + # importing pyautogui on linux can cause errors. + from core.pyautogui_manager import PyAutoGuiSPFManager +else: + from core.tmux_manager import TmuxSPFManager + +logger = logging.getLogger() + +def get_testcases(test_env : Environment, only_run_tests : List[str] = None) -> List[BaseTest]: + res : List[BaseTest] = [] + test_dir = Path(__file__).parent.parent / "tests" + for test_file in test_dir.glob("*_test.py"): + # Import dynamically + module_name = test_file.stem + module = importlib.import_module(f"tests.{module_name}") + for attr_name in dir(module): + if only_run_tests is not None and attr_name not in only_run_tests: + continue + attr = getattr(module, attr_name) + if isinstance(attr, type) and attr is not BaseTest and issubclass(attr, BaseTest) \ + and attr_name.endswith("Test"): + logger.debug("Found a testcase %s, in module %s", attr_name, module_name) + res.append(attr(test_env)) + return res + + +def run_tests(spf_path : Path, stop_on_fail : bool = True, only_run_tests : List[str] = None) -> None: + # is this str conversion needed ? + + spf_manager : BaseSPFManager = None + if platform.system() == "Windows" : + spf_manager = PyAutoGuiSPFManager(str(spf_path)) + else: + spf_manager = TmuxSPFManager(str(spf_path)) + + fs_manager = TestFSManager() + + test_env = Environment(spf_manager, fs_manager) + + try: + cnt_passed : int = 0 + cnt_executed : int = 0 + testcases : List[BaseTest] = get_testcases(test_env, only_run_tests=only_run_tests) + logger.info("Testcases : %s", testcases) + for t in testcases: + logger.info("Running test %s", t) + t.setup() + t.test_execute() + cnt_executed += 1 + + if t.validate(): + logger.info("Passed test %s", t) + cnt_passed += 1 + else: + logger.error("Failed test %s", t) + if stop_on_fail: + break + + logger.info("Finished running %s test. %s passed", cnt_executed, cnt_passed) + finally: + # Make sure of cleanup + # This is still not full proof, as if what happens when TestFSManager __init__ fails ? + test_env.cleanup() + + diff --git a/testsuite/core/spf_manager.py b/testsuite/core/spf_manager.py new file mode 100644 index 00000000..058d6598 --- /dev/null +++ b/testsuite/core/spf_manager.py @@ -0,0 +1,44 @@ +from abc import ABC, abstractmethod +import core.keys as keys + +class BaseSPFManager(ABC): + + def __init__(self, spf_path : str): + self.spf_path = spf_path + # _ denotes the internal variables, anyone should not directly read/modify + self._is_spf_running : bool = False + + @abstractmethod + def start_spf(self, start_dir : str = None) -> None: + pass + + @abstractmethod + def send_text_input(self, text : str, all_at_once : bool = False) -> None: + pass + + @abstractmethod + def send_special_input(self, key : keys.Keys) -> None: + pass + + @abstractmethod + def get_rendered_output(self) -> str: + pass + + + @abstractmethod + def is_spf_running(self) -> bool: + """ + We allow using _is_spf_running variable for efficiency + But this method should give the true state, although this might have some calculations + """ + return self._is_spf_running + + @abstractmethod + def close_spf(self) -> None: + """ + Close spf if its running and cleanup any other resources + """ + + def runtime_info(self) -> str: + return "[No runtime info]" + diff --git a/testsuite/core/test_constants.py b/testsuite/core/test_constants.py new file mode 100644 index 00000000..e375f20b --- /dev/null +++ b/testsuite/core/test_constants.py @@ -0,0 +1,7 @@ +FILE_TEXT1 : str = "This is a sample Text\n" + +KEY_DELAY : float = 0.05 # seconds +OPERATION_DELAY : float = 0.3 # seconds + +# 0.3 second was too less for windows +CLOSE_WAIT_TIME : float = 1 # seconds \ No newline at end of file diff --git a/testsuite/core/tmux_manager.py b/testsuite/core/tmux_manager.py new file mode 100644 index 00000000..7a4e4000 --- /dev/null +++ b/testsuite/core/tmux_manager.py @@ -0,0 +1,72 @@ +import libtmux +import time +import logging +import core.keys as keys +from core.spf_manager import BaseSPFManager + +class TmuxSPFManager(BaseSPFManager): + """ + Tmux based Manager + After running spf, you can connect to the session via + tmux -L superfile attach -t spf_session + Wont work in windows + """ + # Class variables + SPF_START_DELAY : float = 0.1 # seconds + SPF_SOCKET_NAME : str = "superfile" + + # Init should not allocate any resources + def __init__(self, spf_path : str): + super().__init__(spf_path) + self.logger = logging.getLogger() + self.server = libtmux.Server(socket_name=TmuxSPFManager.SPF_SOCKET_NAME) + self.spf_session : libtmux.Session = None + self.spf_pane : libtmux.Pane = None + + def start_spf(self, start_dir : str = None) -> None: + self.spf_session= self.server.new_session('spf_session', + window_command=self.spf_path, + start_directory=start_dir) + time.sleep(TmuxSPFManager.SPF_START_DELAY) + + self.spf_pane = self.spf_session.active_pane + self._is_spf_running = True + + def _send_key(self, key : str) -> None: + self.spf_pane.send_keys(key, enter=False) + + def send_text_input(self, text : str, all_at_once : bool = True) -> None: + if all_at_once: + self._send_key(text) + else: + for c in text: + self._send_key(c) + + def send_special_input(self, key : keys.Keys) -> str: + if key.ascii_code != keys.NO_ASCII: + self._send_key(chr(key.ascii_code)) + elif isinstance(key, keys.SpecialKeys): + self._send_key(key.key_name) + else: + raise Exception(f"Unknown key : {key}") + + def get_rendered_output(self) -> str: + return "[Not supported yet]" + + def is_spf_running(self) -> bool: + self._is_spf_running = (self.spf_session is not None) \ + and (self.spf_session in self.server.sessions) + + return self._is_spf_running + + def close_spf(self) -> None: + if self.is_spf_running(): + self.server.kill_session(self.spf_session.name) + + # Override + def runtime_info(self) -> str: + return str(self.server.sessions) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(server : {self.server}, " + \ + f"session : {self.spf_session}, running : {self._is_spf_running})" diff --git a/testsuite/docs/tmux.md b/testsuite/docs/tmux.md new file mode 100644 index 00000000..5c93110a --- /dev/null +++ b/testsuite/docs/tmux.md @@ -0,0 +1,30 @@ +# Overview +This is to document the behviour of tmux, and how could we use it in testsuite + +# Tmux concepts and working info +- Tmux creates a main server process, and one new process for each session. +image +- `-s` and `-n` for window naming. +- We have prefix keys to send commands to tmux. + +# Sample usage with spf + +## Sending keys to termux and controlling from outside. +image +image + +# Knowledge sharing +- `tmux new 'spf'` - Run spf in tmux +- `tmux attach -t ` attach to an existing session. You can have two windows duplicating same behaviour. +- `tmux kill-session -t ` kill session +- `Ctrl+B`+`:` - Enter commands +- `Ctrl+B`+`D` - Detach from session +- `:source ~/.tmux.conf` - Change the config of running server +- We have already a wrapper libary for termux in python !!!!! +- How to send key press/tmux commands to the process ? + + +# References +- https://github.com/tmux/tmux/wiki/Getting-Started +- https://tao-of-tmux.readthedocs.io/en/latest/manuscript/10-scripting.html#controlling-tmux-send-keys +- https://github.com/tmux-python/libtmux diff --git a/testsuite/main.py b/testsuite/main.py new file mode 100644 index 00000000..61e83519 --- /dev/null +++ b/testsuite/main.py @@ -0,0 +1,61 @@ +import argparse +import logging +import sys +from pathlib import Path + +from core.runner import run_tests +import core.test_constants as tconst + + +def configure_logging(debug : bool = False) -> None: + # Prefer stdout instead of default stderr + handler = logging.StreamHandler(sys.stdout) + + # 7s to align all log levelnames - WARNING is the largest level, with size 7 + handler.setFormatter(logging.Formatter( + '[%(asctime)s - %(levelname)7s] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + )) + + + logger = logging.getLogger() + logger.addHandler(handler) + + if debug: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + logging.getLogger("libtmux").setLevel(logging.WARNING) + +def main(): + # Setup argument parser + parser = argparse.ArgumentParser(description='superfile testsuite') + parser.add_argument('-d', '--debug',action='store_true', + help='Enable debug logging') + parser.add_argument('--close-wait-time', type=float, + help='Override default wait time after closing spf') + parser.add_argument('--spf-path', type=str, + help='Override the default spf executable path(../bin/spf) under test') + parser.add_argument('-t', '--tests', nargs='+', + help='Specify one or more than one space separated testcases to be run') + # Parse arguments + args = parser.parse_args() + if args.close_wait_time is not None: + tconst.CLOSE_WAIT_TIME = args.close_wait_time + + configure_logging(args.debug) + + # Default path + # We maybe should run this only in main.py file. + spf_path = Path(__file__).parent.parent / "bin" / "spf" + + if args.spf_path is not None: + spf_path = Path(args.spf_path) + # Resolve any symlinks, and make it absolute + spf_path = spf_path.resolve() + + run_tests(spf_path, only_run_tests=args.tests) + + +main() diff --git a/testsuite/requirements.txt b/testsuite/requirements.txt new file mode 100644 index 00000000..1a8d3fda --- /dev/null +++ b/testsuite/requirements.txt @@ -0,0 +1,2 @@ +pyautogui +libtmux \ No newline at end of file diff --git a/testsuite/tests/__init__.py b/testsuite/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testsuite/tests/copy_dir_test.py b/testsuite/tests/copy_dir_test.py new file mode 100644 index 00000000..4bc2f456 --- /dev/null +++ b/testsuite/tests/copy_dir_test.py @@ -0,0 +1,32 @@ +from pathlib import Path + +from core.base_test import GenericTestImpl +from core.environment import Environment +import core.test_constants as tconst +import core.keys as keys + +TESTROOT = Path("copy_dir") +DIR1 = TESTROOT / "dir1" +NESTED_DIR1 = DIR1 / "nested1" +NESTED_DIR2 = DIR1 / "nested2" +FILE1 = NESTED_DIR1 / "file1.txt" + +DIR2 = TESTROOT / "dir2" + +DIR1_COPIED = DIR2 / "dir1" +FILE1_COPIED = DIR1_COPIED / "nested1" / "file1.txt" + + + +class CopyDirTest(GenericTestImpl): + + def __init__(self, test_env : Environment): + super().__init__( + test_env=test_env, + test_root=TESTROOT, + start_dir=TESTROOT, + test_dirs=[DIR1, DIR2, NESTED_DIR1, NESTED_DIR2], + test_files=[(FILE1, tconst.FILE_TEXT1)], + key_inputs=[keys.KEY_CTRL_C, keys.KEY_DOWN, keys.KEY_ENTER, keys.KEY_CTRL_V], + validate_exists=[DIR1_COPIED, FILE1_COPIED, DIR1, FILE1] + ) \ No newline at end of file diff --git a/testsuite/tests/copy_test.py b/testsuite/tests/copy_test.py new file mode 100644 index 00000000..c6adcc04 --- /dev/null +++ b/testsuite/tests/copy_test.py @@ -0,0 +1,28 @@ +from pathlib import Path + +from core.base_test import GenericTestImpl +from core.environment import Environment +import core.test_constants as tconst +import core.keys as keys + +TESTROOT = Path("copy_ops") +DIR1 = TESTROOT / "dir1" +DIR2 = TESTROOT / "dir2" +FILE1 = DIR1 / "file1.txt" +FILE1_COPY1 = DIR1 / "file1(1).txt" +FILE1_COPY2 = DIR2 / "file1.txt" + + + +class CopyTest(GenericTestImpl): + + def __init__(self, test_env : Environment): + super().__init__( + test_env=test_env, + test_root=TESTROOT, + start_dir=DIR1, + test_dirs=[DIR1, DIR2], + test_files=[(FILE1, tconst.FILE_TEXT1)], + key_inputs=[keys.KEY_CTRL_C, keys.KEY_CTRL_V], + validate_exists=[FILE1, FILE1_COPY1] + ) \ No newline at end of file diff --git a/testsuite/tests/cut_test.py b/testsuite/tests/cut_test.py new file mode 100644 index 00000000..9f26277f --- /dev/null +++ b/testsuite/tests/cut_test.py @@ -0,0 +1,29 @@ +from pathlib import Path + +from core.base_test import GenericTestImpl +from core.environment import Environment +import core.test_constants as tconst +import core.keys as keys + +TESTROOT = Path("cut_ops") +DIR1 = TESTROOT / "dir1" +DIR2 = TESTROOT / "dir2" +FILE1 = DIR1 / "file1.txt" +FILE1_CUT1 = DIR2 / "file1.txt" + + + +class CutTest(GenericTestImpl): + + def __init__(self, test_env : Environment): + super().__init__( + test_env=test_env, + test_root=TESTROOT, + start_dir=DIR1, + test_dirs=[DIR1, DIR2], + test_files=[(FILE1, tconst.FILE_TEXT1)], + key_inputs=[keys.KEY_CTRL_X, keys.KEY_LEFT, keys.KEY_DOWN, + keys.KEY_ENTER, keys.KEY_CTRL_V], + validate_exists=[FILE1_CUT1], + validate_not_exists=[FILE1] + ) diff --git a/testsuite/tests/delete_test.py b/testsuite/tests/delete_test.py new file mode 100644 index 00000000..333f6134 --- /dev/null +++ b/testsuite/tests/delete_test.py @@ -0,0 +1,25 @@ +from pathlib import Path + +from core.base_test import GenericTestImpl +from core.environment import Environment +import core.test_constants as tconst +import core.keys as keys + +TESTROOT = Path("delete_ops") +# File1 fails in my mac +FILE1 = TESTROOT / "file_to_delete.txt" + + + +class DeleteTest(GenericTestImpl): + + def __init__(self, test_env : Environment): + super().__init__( + test_env=test_env, + test_root=TESTROOT, + start_dir=TESTROOT, + test_dirs=[TESTROOT], + test_files=[(FILE1, tconst.FILE_TEXT1)], + key_inputs=[keys.KEY_CTRL_D, keys.KEY_ENTER], + validate_not_exists=[FILE1] + ) diff --git a/testsuite/tests/rename_test.py b/testsuite/tests/rename_test.py new file mode 100644 index 00000000..aa63bb4b --- /dev/null +++ b/testsuite/tests/rename_test.py @@ -0,0 +1,30 @@ +from pathlib import Path + +from core.base_test import GenericTestImpl +from core.environment import Environment +import core.test_constants as tconst +import core.keys as keys + +TESTROOT = Path("rename_ops") +DIR1 = TESTROOT / "dir1" + +# No extension, as in case of extension, the edit cursor appears before the dot, +# not at the end of filename +FILE1 = DIR1 / "file1" +FILE1_RENAMED = DIR1 / "file2" + + + +class RenameTest(GenericTestImpl): + + def __init__(self, test_env : Environment): + super().__init__( + test_env=test_env, + test_root=TESTROOT, + start_dir=DIR1, + test_dirs=[DIR1], + test_files=[(FILE1, tconst.FILE_TEXT1)], + key_inputs=[keys.KEY_CTRL_R, keys.KEY_BACKSPACE, '2', keys.KEY_ENTER], + validate_exists=[FILE1_RENAMED], + validate_not_exists=[FILE1] + )