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