11 changed files with 1116 additions and 5 deletions
@ -0,0 +1,75 @@ |
|||||||
|
# ttkode Tests |
||||||
|
|
||||||
|
This directory contains the test suite for the ttkode application. |
||||||
|
|
||||||
|
## Test Structure |
||||||
|
|
||||||
|
- **conftest.py** - Pytest configuration and fixtures |
||||||
|
- **test_plugin.py** - Tests for the plugin system (TTkodePlugin and related classes) |
||||||
|
- **test_config.py** - Tests for configuration management (TTKodeCfg) |
||||||
|
- **test_proxy.py** - Tests for proxy functionality (TTKodeViewerProxy, TTKodeProxy) |
||||||
|
- **test_search_file.py** - Tests for file search functionality |
||||||
|
- **test_helper.py** - Tests for helper functions (TTkodeHelper) |
||||||
|
|
||||||
|
## Running Tests |
||||||
|
|
||||||
|
### Run all tests |
||||||
|
|
||||||
|
```bash |
||||||
|
cd apps/ttkode |
||||||
|
pytest tests/ |
||||||
|
``` |
||||||
|
|
||||||
|
### Run specific test file |
||||||
|
|
||||||
|
```bash |
||||||
|
pytest tests/test_plugin.py |
||||||
|
pytest tests/test_config.py |
||||||
|
pytest tests/test_search_file.py |
||||||
|
``` |
||||||
|
|
||||||
|
### Run with verbose output |
||||||
|
|
||||||
|
```bash |
||||||
|
pytest tests/ -v |
||||||
|
``` |
||||||
|
|
||||||
|
### Run with coverage |
||||||
|
|
||||||
|
```bash |
||||||
|
pytest tests/ --cov=ttkode --cov-report=html |
||||||
|
``` |
||||||
|
|
||||||
|
### Run specific test |
||||||
|
|
||||||
|
```bash |
||||||
|
pytest tests/test_plugin.py::TestTTkodePlugin::test_plugin_basic_creation |
||||||
|
``` |
||||||
|
|
||||||
|
## Test Coverage |
||||||
|
|
||||||
|
The test suite covers: |
||||||
|
|
||||||
|
- **Plugin System**: Creation, registration, callbacks, widget management |
||||||
|
- **Configuration**: Default values, saving, loading, path management |
||||||
|
- **Proxy**: Viewer proxy functionality and file name management |
||||||
|
- **File Search**: Pattern matching, .gitignore support, directory walking |
||||||
|
- **Helpers**: Plugin loading and execution |
||||||
|
|
||||||
|
## Notes |
||||||
|
|
||||||
|
Some tests are marked as skipped (`@pytest.mark.skip`) because they require: |
||||||
|
- Full TTKode UI instance setup |
||||||
|
- Plugin folder and file system setup |
||||||
|
- Complex UI mocking infrastructure |
||||||
|
|
||||||
|
These integration tests can be completed when the UI testing infrastructure is available. |
||||||
|
|
||||||
|
## Requirements |
||||||
|
|
||||||
|
Tests require: |
||||||
|
- pytest |
||||||
|
- pyTermTk |
||||||
|
- Python 3.9+ |
||||||
|
|
||||||
|
All dependencies are listed in the main `pyproject.toml` file. |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
# MIT License |
||||||
|
# |
||||||
|
# Copyright (c) 2026 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||||
|
# |
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
# of this software and associated documentation files (the "Software"), to deal |
||||||
|
# in the Software without restriction, including without limitation the rights |
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
# copies of the Software, and to permit persons to whom the Software is |
||||||
|
# furnished to do so, subject to the following conditions: |
||||||
|
# |
||||||
|
# The above copyright notice and this permission notice shall be included in all |
||||||
|
# copies or substantial portions of the Software. |
||||||
|
# |
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
# SOFTWARE. |
||||||
|
|
||||||
|
import sys |
||||||
|
import os |
||||||
|
import pytest |
||||||
|
|
||||||
|
# Add the library path for pyTermTk |
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../../libs/pyTermTk')) |
||||||
|
|
||||||
|
# Add the ttkode package path |
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) |
||||||
|
|
||||||
|
@pytest.fixture(autouse=True) |
||||||
|
def reset_plugin_instances(): |
||||||
|
"""Reset plugin instances before each test to avoid state leakage.""" |
||||||
|
from ttkode.plugin import TTkodePlugin |
||||||
|
original_instances = TTkodePlugin.instances.copy() |
||||||
|
yield |
||||||
|
TTkodePlugin.instances.clear() |
||||||
|
TTkodePlugin.instances.extend(original_instances) |
||||||
@ -0,0 +1,130 @@ |
|||||||
|
# MIT License |
||||||
|
# |
||||||
|
# Copyright (c) 2026 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||||
|
# |
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
# of this software and associated documentation files (the "Software"), to deal |
||||||
|
# in the Software without restriction, including without limitation the rights |
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
# copies of the Software, and to permit persons to whom the Software is |
||||||
|
# furnished to do so, subject to the following conditions: |
||||||
|
# |
||||||
|
# The above copyright notice and this permission notice shall be included in all |
||||||
|
# copies or substantial portions of the Software. |
||||||
|
# |
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
# SOFTWARE. |
||||||
|
|
||||||
|
import pytest |
||||||
|
import os |
||||||
|
import sys |
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../../libs/pyTermTk')) |
||||||
|
|
||||||
|
from ttkode.app.cfg import TTKodeCfg |
||||||
|
from ttkode import __version__ |
||||||
|
|
||||||
|
|
||||||
|
class TestTTKodeCfg: |
||||||
|
"""Test the configuration management system.""" |
||||||
|
|
||||||
|
def test_default_values(self): |
||||||
|
"""Test that configuration has correct default values.""" |
||||||
|
assert TTKodeCfg.version == __version__ |
||||||
|
assert TTKodeCfg.name == "ttkode" |
||||||
|
assert TTKodeCfg.cfgVersion == '1.0' |
||||||
|
assert TTKodeCfg.pathCfg == "." |
||||||
|
assert isinstance(TTKodeCfg.options, dict) |
||||||
|
assert TTKodeCfg.maxsearches == 200 |
||||||
|
|
||||||
|
def test_options_is_dict(self): |
||||||
|
"""Test that options is a dictionary.""" |
||||||
|
assert isinstance(TTKodeCfg.options, dict) |
||||||
|
# Options can be modified |
||||||
|
original_options = TTKodeCfg.options.copy() |
||||||
|
TTKodeCfg.options['test_key'] = 'test_value' |
||||||
|
assert TTKodeCfg.options['test_key'] == 'test_value' |
||||||
|
# Restore original state |
||||||
|
TTKodeCfg.options = original_options |
||||||
|
|
||||||
|
def test_save_creates_directory(self, tmp_path): |
||||||
|
"""Test that save creates the configuration directory if it doesn't exist.""" |
||||||
|
original_path = TTKodeCfg.pathCfg |
||||||
|
TTKodeCfg.pathCfg = str(tmp_path / "config") |
||||||
|
|
||||||
|
# Directory shouldn't exist yet |
||||||
|
assert not os.path.exists(TTKodeCfg.pathCfg) |
||||||
|
|
||||||
|
# Save should create it |
||||||
|
TTKodeCfg.save() |
||||||
|
|
||||||
|
assert os.path.exists(TTKodeCfg.pathCfg) |
||||||
|
assert os.path.isdir(TTKodeCfg.pathCfg) |
||||||
|
|
||||||
|
# Restore original path |
||||||
|
TTKodeCfg.pathCfg = original_path |
||||||
|
|
||||||
|
def test_save_with_different_flags(self, tmp_path): |
||||||
|
"""Test save method with different flag combinations.""" |
||||||
|
original_path = TTKodeCfg.pathCfg |
||||||
|
TTKodeCfg.pathCfg = str(tmp_path / "config2") |
||||||
|
|
||||||
|
# Test with all flags |
||||||
|
TTKodeCfg.save(searches=True, filters=True, colors=True, options=True) |
||||||
|
assert os.path.exists(TTKodeCfg.pathCfg) |
||||||
|
|
||||||
|
# Test with individual flags |
||||||
|
TTKodeCfg.save(searches=False, filters=False, colors=False, options=True) |
||||||
|
TTKodeCfg.save(searches=True, filters=False, colors=False, options=False) |
||||||
|
TTKodeCfg.save(searches=False, filters=True, colors=False, options=False) |
||||||
|
TTKodeCfg.save(searches=False, filters=False, colors=True, options=False) |
||||||
|
|
||||||
|
# Restore original path |
||||||
|
TTKodeCfg.pathCfg = original_path |
||||||
|
|
||||||
|
def test_maxsearches_value(self): |
||||||
|
"""Test that maxsearches has a reasonable default value.""" |
||||||
|
assert isinstance(TTKodeCfg.maxsearches, int) |
||||||
|
assert TTKodeCfg.maxsearches > 0 |
||||||
|
assert TTKodeCfg.maxsearches == 200 |
||||||
|
|
||||||
|
def test_version_matches_package_version(self): |
||||||
|
"""Test that config version matches package version.""" |
||||||
|
from ttkode import __version__ as pkg_version |
||||||
|
assert TTKodeCfg.version == pkg_version |
||||||
|
|
||||||
|
def test_config_path_modification(self, tmp_path): |
||||||
|
"""Test that pathCfg can be modified.""" |
||||||
|
original_path = TTKodeCfg.pathCfg |
||||||
|
new_path = str(tmp_path / "new_config") |
||||||
|
|
||||||
|
TTKodeCfg.pathCfg = new_path |
||||||
|
assert TTKodeCfg.pathCfg == new_path |
||||||
|
|
||||||
|
# Restore original path |
||||||
|
TTKodeCfg.pathCfg = original_path |
||||||
|
|
||||||
|
def test_options_persistence(self): |
||||||
|
"""Test that options can be set and retrieved.""" |
||||||
|
original_options = TTKodeCfg.options.copy() |
||||||
|
|
||||||
|
# Set some options |
||||||
|
test_options = { |
||||||
|
'editor.fontSize': 12, |
||||||
|
'editor.tabSize': 4, |
||||||
|
'theme': 'dark' |
||||||
|
} |
||||||
|
TTKodeCfg.options = test_options |
||||||
|
|
||||||
|
# Verify options are set |
||||||
|
assert TTKodeCfg.options['editor.fontSize'] == 12 |
||||||
|
assert TTKodeCfg.options['editor.tabSize'] == 4 |
||||||
|
assert TTKodeCfg.options['theme'] == 'dark' |
||||||
|
|
||||||
|
# Restore original options |
||||||
|
TTKodeCfg.options = original_options |
||||||
@ -0,0 +1,194 @@ |
|||||||
|
# MIT License |
||||||
|
# |
||||||
|
# Copyright (c) 2026 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||||
|
# |
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
# of this software and associated documentation files (the "Software"), to deal |
||||||
|
# in the Software without restriction, including without limitation the rights |
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
# copies of the Software, and to permit persons to whom the Software is |
||||||
|
# furnished to do so, subject to the following conditions: |
||||||
|
# |
||||||
|
# The above copyright notice and this permission notice shall be included in all |
||||||
|
# copies or substantial portions of the Software. |
||||||
|
# |
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
# SOFTWARE. |
||||||
|
|
||||||
|
import pytest |
||||||
|
import sys |
||||||
|
import os |
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../../libs/pyTermTk')) |
||||||
|
|
||||||
|
import TermTk as ttk |
||||||
|
from ttkode.helper import TTkodeHelper |
||||||
|
from ttkode.plugin import TTkodePlugin, TTkodePluginWidgetActivity, TTkodePluginWidgetPanel |
||||||
|
|
||||||
|
|
||||||
|
class TestTTkodeHelper: |
||||||
|
"""Test the helper functions for plugin management.""" |
||||||
|
|
||||||
|
def test_get_plugins_empty(self): |
||||||
|
"""Test getting plugins when none are loaded.""" |
||||||
|
# Clear existing plugins |
||||||
|
TTkodePlugin.instances.clear() |
||||||
|
|
||||||
|
plugins = TTkodeHelper._getPlugins() |
||||||
|
|
||||||
|
assert plugins == [] |
||||||
|
assert isinstance(plugins, list) |
||||||
|
|
||||||
|
def test_get_plugins_with_instances(self): |
||||||
|
"""Test getting plugins after some are created.""" |
||||||
|
# Clear and create test plugins |
||||||
|
TTkodePlugin.instances.clear() |
||||||
|
|
||||||
|
plugin1 = TTkodePlugin(name="TestPlugin1") |
||||||
|
plugin2 = TTkodePlugin(name="TestPlugin2") |
||||||
|
|
||||||
|
plugins = TTkodeHelper._getPlugins() |
||||||
|
|
||||||
|
assert len(plugins) == 2 |
||||||
|
assert plugin1 in plugins |
||||||
|
assert plugin2 in plugins |
||||||
|
|
||||||
|
def test_get_plugins_returns_reference(self): |
||||||
|
"""Test that _getPlugins returns a reference to the instances list.""" |
||||||
|
TTkodePlugin.instances.clear() |
||||||
|
|
||||||
|
plugins = TTkodeHelper._getPlugins() |
||||||
|
|
||||||
|
# Should be the same list object |
||||||
|
assert plugins is TTkodePlugin.instances |
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Requires plugin folder and file system setup") |
||||||
|
def test_load_plugins_from_folder(self, tmp_path): |
||||||
|
"""Test loading plugins from a folder.""" |
||||||
|
# This would require creating actual plugin files |
||||||
|
# and modifying the plugin folder path |
||||||
|
pass |
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Requires full TTKode UI instance") |
||||||
|
def test_run_plugins_with_activity_widgets(self): |
||||||
|
"""Test running plugins with activity widgets.""" |
||||||
|
# This requires a full TTKode instance with activity bar |
||||||
|
pass |
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Requires full TTKode UI instance") |
||||||
|
def test_run_plugins_with_panel_widgets(self): |
||||||
|
"""Test running plugins with panel widgets.""" |
||||||
|
# This requires a full TTKode instance with panels |
||||||
|
pass |
||||||
|
|
||||||
|
def test_plugin_init_execution(self): |
||||||
|
"""Test that plugin init callbacks are executed.""" |
||||||
|
TTkodePlugin.instances.clear() |
||||||
|
|
||||||
|
init_executed = [] |
||||||
|
|
||||||
|
def init_callback(): |
||||||
|
init_executed.append(True) |
||||||
|
|
||||||
|
plugin = TTkodePlugin(name="InitPlugin", init=init_callback) |
||||||
|
|
||||||
|
# Simulate what _loadPlugins does |
||||||
|
for mod in TTkodePlugin.instances: |
||||||
|
if mod.init is not None: |
||||||
|
mod.init() |
||||||
|
|
||||||
|
assert len(init_executed) == 1 |
||||||
|
|
||||||
|
def test_plugin_apply_execution(self): |
||||||
|
"""Test that plugin apply callbacks are executed.""" |
||||||
|
TTkodePlugin.instances.clear() |
||||||
|
|
||||||
|
apply_executed = [] |
||||||
|
|
||||||
|
def apply_callback(): |
||||||
|
apply_executed.append(True) |
||||||
|
|
||||||
|
plugin = TTkodePlugin(name="ApplyPlugin", apply=apply_callback) |
||||||
|
|
||||||
|
# Simulate what _runPlugins does |
||||||
|
for mod in TTkodePlugin.instances: |
||||||
|
if mod.apply is not None: |
||||||
|
mod.apply() |
||||||
|
|
||||||
|
assert len(apply_executed) == 1 |
||||||
|
|
||||||
|
def test_multiple_plugins_init(self): |
||||||
|
"""Test initializing multiple plugins.""" |
||||||
|
TTkodePlugin.instances.clear() |
||||||
|
|
||||||
|
counters = {'plugin1': 0, 'plugin2': 0, 'plugin3': 0} |
||||||
|
|
||||||
|
def make_init(name): |
||||||
|
def init_cb(): |
||||||
|
counters[name] += 1 |
||||||
|
return init_cb |
||||||
|
|
||||||
|
plugin1 = TTkodePlugin(name="Plugin1", init=make_init('plugin1')) |
||||||
|
plugin2 = TTkodePlugin(name="Plugin2", init=make_init('plugin2')) |
||||||
|
plugin3 = TTkodePlugin(name="Plugin3", init=make_init('plugin3')) |
||||||
|
|
||||||
|
# Execute all init callbacks |
||||||
|
for mod in TTkodePlugin.instances: |
||||||
|
if mod.init is not None: |
||||||
|
mod.init() |
||||||
|
|
||||||
|
assert counters['plugin1'] == 1 |
||||||
|
assert counters['plugin2'] == 1 |
||||||
|
assert counters['plugin3'] == 1 |
||||||
|
|
||||||
|
def test_plugin_with_widgets_detection(self): |
||||||
|
"""Test detecting plugin widgets.""" |
||||||
|
TTkodePlugin.instances.clear() |
||||||
|
|
||||||
|
activity_widget = TTkodePluginWidgetActivity( |
||||||
|
widget=ttk.TTkWidget(), |
||||||
|
activityName="TestActivity", |
||||||
|
icon=ttk.TTkString("📁") |
||||||
|
) |
||||||
|
|
||||||
|
panel_widget = TTkodePluginWidgetPanel( |
||||||
|
widget=ttk.TTkWidget(), |
||||||
|
panelName="TestPanel" |
||||||
|
) |
||||||
|
|
||||||
|
plugin = TTkodePlugin( |
||||||
|
name="WidgetPlugin", |
||||||
|
widgets=[activity_widget, panel_widget] |
||||||
|
) |
||||||
|
|
||||||
|
# Verify plugin has widgets |
||||||
|
assert len(plugin.widgets) == 2 |
||||||
|
|
||||||
|
# Count widget types |
||||||
|
activity_count = sum(1 for w in plugin.widgets if isinstance(w, TTkodePluginWidgetActivity)) |
||||||
|
panel_count = sum(1 for w in plugin.widgets if isinstance(w, TTkodePluginWidgetPanel)) |
||||||
|
|
||||||
|
assert activity_count == 1 |
||||||
|
assert panel_count == 1 |
||||||
|
|
||||||
|
def test_plugin_without_callbacks(self): |
||||||
|
"""Test plugin without any callbacks doesn't crash.""" |
||||||
|
TTkodePlugin.instances.clear() |
||||||
|
|
||||||
|
plugin = TTkodePlugin(name="MinimalPlugin") |
||||||
|
|
||||||
|
# These should not raise exceptions |
||||||
|
if plugin.init is not None: |
||||||
|
plugin.init() |
||||||
|
if plugin.apply is not None: |
||||||
|
plugin.apply() |
||||||
|
if plugin.run is not None: |
||||||
|
plugin.run() |
||||||
|
|
||||||
|
# If we get here, no exceptions were raised |
||||||
|
assert True |
||||||
@ -0,0 +1,120 @@ |
|||||||
|
# MIT License |
||||||
|
# |
||||||
|
# Copyright (c) 2026 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||||
|
# |
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
# of this software and associated documentation files (the "Software"), to deal |
||||||
|
# in the Software without restriction, including without limitation the rights |
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
# copies of the Software, and to permit persons to whom the Software is |
||||||
|
# furnished to do so, subject to the following conditions: |
||||||
|
# |
||||||
|
# The above copyright notice and this permission notice shall be included in all |
||||||
|
# copies or substantial portions of the Software. |
||||||
|
# |
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
# SOFTWARE. |
||||||
|
|
||||||
|
import pytest |
||||||
|
import sys |
||||||
|
import os |
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../../libs/pyTermTk')) |
||||||
|
|
||||||
|
from ttkode.proxy import TTKodeViewerProxy |
||||||
|
|
||||||
|
|
||||||
|
class TestTTKodeViewerProxy: |
||||||
|
"""Test the viewer proxy class.""" |
||||||
|
|
||||||
|
def test_viewer_proxy_creation(self): |
||||||
|
"""Test creating a viewer proxy with a filename.""" |
||||||
|
filename = "/path/to/test/file.py" |
||||||
|
proxy = TTKodeViewerProxy(filename) |
||||||
|
|
||||||
|
assert proxy.fileName() == filename |
||||||
|
|
||||||
|
def test_viewer_proxy_empty_filename(self): |
||||||
|
"""Test viewer proxy with an empty filename.""" |
||||||
|
proxy = TTKodeViewerProxy("") |
||||||
|
assert proxy.fileName() == "" |
||||||
|
|
||||||
|
def test_viewer_proxy_relative_path(self): |
||||||
|
"""Test viewer proxy with a relative path.""" |
||||||
|
filename = "relative/path/file.txt" |
||||||
|
proxy = TTKodeViewerProxy(filename) |
||||||
|
assert proxy.fileName() == filename |
||||||
|
|
||||||
|
def test_viewer_proxy_absolute_path(self): |
||||||
|
"""Test viewer proxy with an absolute path.""" |
||||||
|
filename = "/absolute/path/to/file.py" |
||||||
|
proxy = TTKodeViewerProxy(filename) |
||||||
|
assert proxy.fileName() == filename |
||||||
|
|
||||||
|
def test_viewer_proxy_with_special_chars(self): |
||||||
|
"""Test viewer proxy with special characters in filename.""" |
||||||
|
filename = "/path/with spaces/and-dashes/file_name.py" |
||||||
|
proxy = TTKodeViewerProxy(filename) |
||||||
|
assert proxy.fileName() == filename |
||||||
|
|
||||||
|
def test_multiple_viewer_proxies(self): |
||||||
|
"""Test creating multiple viewer proxies.""" |
||||||
|
filenames = [ |
||||||
|
"/path/file1.py", |
||||||
|
"/path/file2.txt", |
||||||
|
"/path/file3.md" |
||||||
|
] |
||||||
|
|
||||||
|
proxies = [TTKodeViewerProxy(fn) for fn in filenames] |
||||||
|
|
||||||
|
for proxy, filename in zip(proxies, filenames): |
||||||
|
assert proxy.fileName() == filename |
||||||
|
|
||||||
|
def test_viewer_proxy_filename_immutability(self): |
||||||
|
"""Test that filename is set during initialization and retrieved correctly.""" |
||||||
|
original_filename = "/original/path/file.py" |
||||||
|
proxy = TTKodeViewerProxy(original_filename) |
||||||
|
|
||||||
|
# Verify we can retrieve it |
||||||
|
assert proxy.fileName() == original_filename |
||||||
|
|
||||||
|
# Note: The proxy doesn't expose a setter, so filename should remain stable |
||||||
|
|
||||||
|
|
||||||
|
# Note: TTKodeProxy requires a full TTKode instance which depends on the UI framework |
||||||
|
# These tests would need a more complex setup with mocked UI components |
||||||
|
# For now, we test the simpler TTKodeViewerProxy |
||||||
|
# Integration tests for TTKodeProxy should be added when UI mocking infrastructure is available |
||||||
|
|
||||||
|
class TestTTKodeProxyIntegration: |
||||||
|
"""Integration tests for TTKodeProxy - these require more setup.""" |
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Requires full TTKode UI instance setup") |
||||||
|
def test_proxy_ttkode_reference(self): |
||||||
|
"""Test that proxy maintains reference to TTKode instance.""" |
||||||
|
# This would require setting up a full TTKode instance |
||||||
|
# which depends on the TTk UI framework |
||||||
|
pass |
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Requires full TTKode UI instance setup") |
||||||
|
def test_proxy_open_file(self): |
||||||
|
"""Test opening a file through the proxy.""" |
||||||
|
# This would require a full TTKode instance |
||||||
|
pass |
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Requires full TTKode UI instance setup") |
||||||
|
def test_proxy_iter_widgets(self): |
||||||
|
"""Test iterating through widgets via proxy.""" |
||||||
|
# This would require a full TTKode instance with widgets |
||||||
|
pass |
||||||
|
|
||||||
|
@pytest.mark.skip(reason="Requires full TTKode UI instance setup") |
||||||
|
def test_proxy_close_tab(self): |
||||||
|
"""Test closing a tab through the proxy.""" |
||||||
|
# This would require a full TTKode instance with tabs |
||||||
|
pass |
||||||
@ -0,0 +1,314 @@ |
|||||||
|
# MIT License |
||||||
|
# |
||||||
|
# Copyright (c) 2026 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com> |
||||||
|
# |
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy |
||||||
|
# of this software and associated documentation files (the "Software"), to deal |
||||||
|
# in the Software without restriction, including without limitation the rights |
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||||
|
# copies of the Software, and to permit persons to whom the Software is |
||||||
|
# furnished to do so, subject to the following conditions: |
||||||
|
# |
||||||
|
# The above copyright notice and this permission notice shall be included in all |
||||||
|
# copies or substantial portions of the Software. |
||||||
|
# |
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
# SOFTWARE. |
||||||
|
|
||||||
|
import pytest |
||||||
|
import sys |
||||||
|
import os |
||||||
|
from pathlib import Path |
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../../libs/pyTermTk')) |
||||||
|
|
||||||
|
from ttkode.app.helpers.search_file import ( |
||||||
|
is_text_file, |
||||||
|
_load_gitignore_patterns, |
||||||
|
_glob_match_patterns, |
||||||
|
_custom_walk, |
||||||
|
TTKode_SearchFile |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class TestIsTextFile: |
||||||
|
"""Test text file detection.""" |
||||||
|
|
||||||
|
def test_text_file_detection(self, tmp_path): |
||||||
|
"""Test detection of text files.""" |
||||||
|
text_file = tmp_path / "test.txt" |
||||||
|
text_file.write_text("This is a text file\nwith multiple lines") |
||||||
|
|
||||||
|
assert is_text_file(str(text_file)) is True |
||||||
|
|
||||||
|
def test_python_file_detection(self, tmp_path): |
||||||
|
"""Test Python files are detected as text.""" |
||||||
|
py_file = tmp_path / "test.py" |
||||||
|
py_file.write_text("print('hello world')") |
||||||
|
|
||||||
|
assert is_text_file(str(py_file)) is True |
||||||
|
|
||||||
|
def test_json_file_detection(self, tmp_path): |
||||||
|
"""Test JSON files are detected as text.""" |
||||||
|
json_file = tmp_path / "test.json" |
||||||
|
json_file.write_text('{"key": "value"}') |
||||||
|
|
||||||
|
assert is_text_file(str(json_file)) is True |
||||||
|
|
||||||
|
def test_binary_file_detection(self, tmp_path): |
||||||
|
"""Test binary files are detected correctly.""" |
||||||
|
binary_file = tmp_path / "test.bin" |
||||||
|
binary_file.write_bytes(b'\x00\x01\x02\x03\x04\x05') |
||||||
|
|
||||||
|
assert is_text_file(str(binary_file)) is False |
||||||
|
|
||||||
|
def test_empty_file(self, tmp_path): |
||||||
|
"""Test empty file detection.""" |
||||||
|
empty_file = tmp_path / "empty.txt" |
||||||
|
empty_file.write_text("") |
||||||
|
|
||||||
|
# Empty files might be considered text depending on implementation |
||||||
|
result = is_text_file(str(empty_file)) |
||||||
|
assert isinstance(result, bool) |
||||||
|
|
||||||
|
|
||||||
|
class TestLoadGitignorePatterns: |
||||||
|
"""Test loading .gitignore patterns.""" |
||||||
|
|
||||||
|
def test_load_existing_gitignore(self, tmp_path): |
||||||
|
"""Test loading an existing .gitignore file.""" |
||||||
|
gitignore = tmp_path / ".gitignore" |
||||||
|
gitignore.write_text("*.pyc\n__pycache__/\n.env\n") |
||||||
|
|
||||||
|
patterns = _load_gitignore_patterns(str(gitignore)) |
||||||
|
|
||||||
|
assert "*.pyc" in patterns |
||||||
|
assert "__pycache__/" in patterns |
||||||
|
assert ".env" in patterns |
||||||
|
|
||||||
|
def test_load_nonexistent_gitignore(self, tmp_path): |
||||||
|
"""Test loading a non-existent .gitignore file.""" |
||||||
|
gitignore_path = str(tmp_path / ".gitignore") |
||||||
|
patterns = _load_gitignore_patterns(gitignore_path) |
||||||
|
|
||||||
|
assert patterns == [] |
||||||
|
|
||||||
|
def test_empty_gitignore(self, tmp_path): |
||||||
|
"""Test loading an empty .gitignore file.""" |
||||||
|
gitignore = tmp_path / ".gitignore" |
||||||
|
gitignore.write_text("") |
||||||
|
|
||||||
|
patterns = _load_gitignore_patterns(str(gitignore)) |
||||||
|
assert patterns == [] |
||||||
|
|
||||||
|
def test_gitignore_with_comments(self, tmp_path): |
||||||
|
"""Test .gitignore with comments.""" |
||||||
|
gitignore = tmp_path / ".gitignore" |
||||||
|
gitignore.write_text("# This is a comment\n*.pyc\n# Another comment\n__pycache__/") |
||||||
|
|
||||||
|
patterns = _load_gitignore_patterns(str(gitignore)) |
||||||
|
|
||||||
|
# Comments should be included as-is (filtering happens in matching) |
||||||
|
assert len(patterns) == 4 |
||||||
|
|
||||||
|
|
||||||
|
class TestGlobMatchPatterns: |
||||||
|
"""Test glob pattern matching.""" |
||||||
|
|
||||||
|
def test_simple_pattern_match(self): |
||||||
|
"""Test simple pattern matching.""" |
||||||
|
assert _glob_match_patterns("test.pyc", ["*.pyc"]) is True |
||||||
|
assert _glob_match_patterns("test.py", ["*.pyc"]) is False |
||||||
|
|
||||||
|
def test_directory_pattern_match(self): |
||||||
|
"""Test directory pattern matching.""" |
||||||
|
assert _glob_match_patterns("./__pycache__/module.pyc", ["__pycache__"]) is True |
||||||
|
assert _glob_match_patterns("./src/__pycache__/", ["__pycache__"]) is True |
||||||
|
|
||||||
|
def test_multiple_patterns(self): |
||||||
|
"""Test matching against multiple patterns.""" |
||||||
|
patterns = ["*.pyc", "*.pyo", "__pycache__"] |
||||||
|
|
||||||
|
assert _glob_match_patterns("test.pyc", patterns) is True |
||||||
|
assert _glob_match_patterns("test.pyo", patterns) is True |
||||||
|
assert _glob_match_patterns("./__pycache__/", patterns) is True |
||||||
|
assert _glob_match_patterns("test.py", patterns) is False |
||||||
|
|
||||||
|
def test_empty_patterns(self): |
||||||
|
"""Test with empty pattern list.""" |
||||||
|
assert _glob_match_patterns("test.py", []) is False |
||||||
|
|
||||||
|
def test_current_directory(self): |
||||||
|
"""Test matching with current directory.""" |
||||||
|
result = _glob_match_patterns(".", ["*.pyc"]) |
||||||
|
assert isinstance(result, bool) |
||||||
|
|
||||||
|
def test_relative_path_matching(self): |
||||||
|
"""Test matching relative paths.""" |
||||||
|
assert _glob_match_patterns("./src/module.pyc", ["*.pyc"]) is True |
||||||
|
assert _glob_match_patterns("src/module.py", ["*.pyc"]) is False |
||||||
|
|
||||||
|
|
||||||
|
class TestCustomWalk: |
||||||
|
"""Test custom directory walking.""" |
||||||
|
|
||||||
|
def test_walk_simple_directory(self, tmp_path): |
||||||
|
"""Test walking a simple directory structure.""" |
||||||
|
# Create test structure |
||||||
|
(tmp_path / "file1.py").write_text("print('file1')") |
||||||
|
(tmp_path / "file2.txt").write_text("text") |
||||||
|
|
||||||
|
results = list(_custom_walk(str(tmp_path))) |
||||||
|
|
||||||
|
assert len(results) == 2 |
||||||
|
filenames = [entry[1] for entry in results] |
||||||
|
assert "file1.py" in filenames |
||||||
|
assert "file2.txt" in filenames |
||||||
|
|
||||||
|
def test_walk_nested_directories(self, tmp_path): |
||||||
|
"""Test walking nested directories.""" |
||||||
|
# Create nested structure |
||||||
|
subdir = tmp_path / "subdir" |
||||||
|
subdir.mkdir() |
||||||
|
(tmp_path / "root.py").write_text("root") |
||||||
|
(subdir / "nested.py").write_text("nested") |
||||||
|
|
||||||
|
results = list(_custom_walk(str(tmp_path))) |
||||||
|
|
||||||
|
assert len(results) == 2 |
||||||
|
filenames = [entry[1] for entry in results] |
||||||
|
assert "root.py" in filenames |
||||||
|
assert "nested.py" in filenames |
||||||
|
|
||||||
|
def test_walk_with_exclude_patterns(self, tmp_path): |
||||||
|
"""Test walking with exclusion patterns.""" |
||||||
|
# Create files |
||||||
|
(tmp_path / "include.py").write_text("include") |
||||||
|
(tmp_path / "exclude.pyc").write_text("exclude") |
||||||
|
|
||||||
|
results = list(_custom_walk(str(tmp_path), exclude_patterns=["*.pyc"])) |
||||||
|
|
||||||
|
filenames = [entry[1] for entry in results] |
||||||
|
assert "include.py" in filenames |
||||||
|
assert "exclude.pyc" not in filenames |
||||||
|
|
||||||
|
def test_walk_with_include_patterns(self, tmp_path): |
||||||
|
"""Test walking with inclusion patterns.""" |
||||||
|
# Create files |
||||||
|
(tmp_path / "test.py").write_text("python") |
||||||
|
(tmp_path / "test.txt").write_text("text") |
||||||
|
|
||||||
|
results = list(_custom_walk(str(tmp_path), include_patterns=["*.py"])) |
||||||
|
|
||||||
|
filenames = [entry[1] for entry in results] |
||||||
|
assert "test.py" in filenames |
||||||
|
assert "test.txt" not in filenames |
||||||
|
|
||||||
|
def test_walk_ignores_git_directory(self, tmp_path): |
||||||
|
"""Test that .git directories are ignored.""" |
||||||
|
# Create .git directory |
||||||
|
git_dir = tmp_path / ".git" |
||||||
|
git_dir.mkdir() |
||||||
|
(git_dir / "config").write_text("git config") |
||||||
|
(tmp_path / "normal.py").write_text("normal file") |
||||||
|
|
||||||
|
results = list(_custom_walk(str(tmp_path))) |
||||||
|
|
||||||
|
filenames = [entry[1] for entry in results] |
||||||
|
assert "normal.py" in filenames |
||||||
|
assert "config" not in filenames |
||||||
|
|
||||||
|
def test_walk_respects_gitignore(self, tmp_path): |
||||||
|
"""Test that .gitignore patterns are respected.""" |
||||||
|
# Create .gitignore |
||||||
|
gitignore = tmp_path / ".gitignore" |
||||||
|
gitignore.write_text("*.pyc\n__pycache__/") |
||||||
|
|
||||||
|
# Create files |
||||||
|
(tmp_path / "normal.py").write_text("include") |
||||||
|
(tmp_path / "compiled.pyc").write_text("exclude") |
||||||
|
|
||||||
|
results = list(_custom_walk(str(tmp_path))) |
||||||
|
|
||||||
|
filenames = [entry[1] for entry in results] |
||||||
|
assert "normal.py" in filenames |
||||||
|
assert ".gitignore" in filenames |
||||||
|
assert "compiled.pyc" not in filenames |
||||||
|
|
||||||
|
|
||||||
|
class TestTTKodeSearchFile: |
||||||
|
"""Test the file search functionality.""" |
||||||
|
|
||||||
|
def test_search_by_pattern(self, tmp_path): |
||||||
|
"""Test searching files by pattern.""" |
||||||
|
# Create test files |
||||||
|
pass |
||||||
|
(tmp_path / "test_Eugenio_ABC_file.py").write_text("test") |
||||||
|
(tmp_path / "other.txt").write_text("other") |
||||||
|
(tmp_path / "test_Eugenio_ABC_data.json").write_text("{}") |
||||||
|
|
||||||
|
results = list(TTKode_SearchFile.getFilesFromPattern(tmp_path, "test_Eugenio_ABC")) |
||||||
|
|
||||||
|
assert len(results) == 2 |
||||||
|
result_names = [r.name for r in results] |
||||||
|
assert "test_Eugenio_ABC_file.py" in result_names |
||||||
|
assert "test_Eugenio_ABC_data.json" in result_names |
||||||
|
assert "other.txt" not in result_names |
||||||
|
|
||||||
|
def test_search_empty_pattern(self, tmp_path): |
||||||
|
"""Test searching with empty pattern matches all files.""" |
||||||
|
(tmp_path / "file1.py").write_text("1") |
||||||
|
(tmp_path / "file2.txt").write_text("2") |
||||||
|
|
||||||
|
results = list(TTKode_SearchFile.getFilesFromPattern(tmp_path, "")) |
||||||
|
|
||||||
|
# Empty pattern should match all files |
||||||
|
assert len(results) >= 2 |
||||||
|
|
||||||
|
def test_search_no_matches(self, tmp_path): |
||||||
|
"""Test searching with pattern that matches nothing.""" |
||||||
|
(tmp_path / "file1.py").write_text("1") |
||||||
|
|
||||||
|
results = list(TTKode_SearchFile.getFilesFromPattern(tmp_path, "nonexistent_pattern_xyz")) |
||||||
|
|
||||||
|
assert len(results) == 0 |
||||||
|
|
||||||
|
def test_search_nested_directories(self, tmp_path): |
||||||
|
"""Test searching in nested directories.""" |
||||||
|
# Create nested structure |
||||||
|
subdir = tmp_path / "subdir" |
||||||
|
subdir.mkdir() |
||||||
|
(tmp_path / "root_test.py").write_text("root") |
||||||
|
(subdir / "nested_test.py").write_text("nested") |
||||||
|
|
||||||
|
results = list(TTKode_SearchFile.getFilesFromPattern(tmp_path, "test")) |
||||||
|
|
||||||
|
assert len(results) == 2 |
||||||
|
result_names = [r.name for r in results] |
||||||
|
assert "root_test.py" in result_names |
||||||
|
assert "nested_test.py" in result_names |
||||||
|
|
||||||
|
def test_search_returns_path_objects(self, tmp_path): |
||||||
|
"""Test that search returns Path objects.""" |
||||||
|
(tmp_path / "test.py").write_text("test") |
||||||
|
|
||||||
|
results = list(TTKode_SearchFile.getFilesFromPattern(tmp_path, "test")) |
||||||
|
|
||||||
|
assert len(results) > 0 |
||||||
|
for result in results: |
||||||
|
assert isinstance(result, Path) |
||||||
|
|
||||||
|
def test_search_case_sensitivity(self, tmp_path): |
||||||
|
"""Test search pattern case sensitivity.""" |
||||||
|
(tmp_path / "TestFile.py").write_text("test") |
||||||
|
(tmp_path / "testfile.txt").write_text("test") |
||||||
|
|
||||||
|
results = list(TTKode_SearchFile.getFilesFromPattern(tmp_path, "test")) |
||||||
|
|
||||||
|
# Should match both (case-insensitive glob matching) |
||||||
|
assert len(results) >= 2 |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
import TermTk as ttk |
||||||
|
|
||||||
|
class TTkode_Terminal(ttk.TTkTerminal): |
||||||
|
__slots__ = ('_isRunning') |
||||||
|
def __init__(self, **kwargs): |
||||||
|
self._isRunning = False |
||||||
|
super().__init__(**kwargs) |
||||||
|
|
||||||
|
def show(self): |
||||||
|
if not self._isRunning: |
||||||
|
self._isRunning = True |
||||||
|
_th = ttk.TTkTerminalHelper(term=self) |
||||||
|
_th.runShell() |
||||||
|
return super().show() |
||||||
Loading…
Reference in new issue