Browse Source

choe: add tests (#606)

pull/607/head
Pier CeccoPierangioliEugenio 2 weeks ago committed by GitHub
parent
commit
e2f325debb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      .vscode/settings.json
  2. 1
      apps/ttkode/pyproject.toml
  3. 75
      apps/ttkode/tests/README.md
  4. 40
      apps/ttkode/tests/conftest.py
  5. 130
      apps/ttkode/tests/test_config.py
  6. 194
      apps/ttkode/tests/test_helper.py
  7. 212
      apps/ttkode/tests/test_plugin.py
  8. 120
      apps/ttkode/tests/test_proxy.py
  9. 314
      apps/ttkode/tests/test_search_file.py
  10. 18
      apps/ttkode/ttkode/app/ttkode.py
  11. 14
      apps/ttkode/ttkode/app/ttkode_terminal.py

3
.vscode/settings.json vendored

@ -1,7 +1,8 @@
{ {
"python.testing.pytestArgs": [ "python.testing.pytestArgs": [
"-vv", "--color=yes", "-s",
"tests/pytest", "tests/pytest",
"-vv", "--color=yes", "-s" "apps/",
], ],
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true, "python.testing.pytestEnabled": true,

1
apps/ttkode/pyproject.toml

@ -25,6 +25,7 @@ classifiers = [
] ]
dependencies = [ dependencies = [
'pyTermTk>=0.48.1-a0', 'pyTermTk>=0.48.1-a0',
'pytest',
'appdirs', 'appdirs',
'copykitten', 'copykitten',
'pygments' 'pygments'

75
apps/ttkode/tests/README.md

@ -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.

40
apps/ttkode/tests/conftest.py

@ -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)

130
apps/ttkode/tests/test_config.py

@ -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

194
apps/ttkode/tests/test_helper.py

@ -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

212
apps/ttkode/tests/test_plugin.py

@ -0,0 +1,212 @@
# 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.plugin import (
TTkodePlugin,
TTkodePluginWidget,
TTkodePluginWidgetActivity,
TTkodePluginWidgetPanel
)
class TestTTkodePluginWidget:
"""Test the base plugin widget class."""
def test_plugin_widget_creation(self):
"""Test creating a basic plugin widget."""
widget = ttk.TTkWidget()
plugin_widget = TTkodePluginWidget(widget=widget)
assert plugin_widget.widget is widget
def test_plugin_widget_activity_creation(self):
"""Test creating an activity plugin widget."""
widget = ttk.TTkWidget()
icon = ttk.TTkString("📁")
name = "TestActivity"
activity_widget = TTkodePluginWidgetActivity(
widget=widget,
activityName=name,
icon=icon
)
assert activity_widget.widget is widget
assert activity_widget.activityName == name
assert activity_widget.icon == icon
def test_plugin_widget_panel_creation(self):
"""Test creating a panel plugin widget."""
widget = ttk.TTkWidget()
panel_name = "TestPanel"
panel_widget = TTkodePluginWidgetPanel(
widget=widget,
panelName=panel_name
)
assert panel_widget.widget is widget
assert panel_widget.panelName == panel_name
class TestTTkodePlugin:
"""Test the plugin system."""
def test_plugin_basic_creation(self):
"""Test creating a basic plugin."""
plugin = TTkodePlugin(name="TestPlugin")
assert plugin.name == "TestPlugin"
assert plugin.init is None
assert plugin.apply is None
assert plugin.run is None
assert plugin.widgets == []
assert plugin in TTkodePlugin.instances
def test_plugin_with_callbacks(self):
"""Test plugin with init, apply, and run callbacks."""
init_called = []
apply_called = []
run_called = []
def init_cb():
init_called.append(True)
def apply_cb():
apply_called.append(True)
def run_cb():
run_called.append(True)
plugin = TTkodePlugin(
name="CallbackPlugin",
init=init_cb,
apply=apply_cb,
run=run_cb
)
# Execute callbacks
if plugin.init:
plugin.init()
if plugin.apply:
plugin.apply()
if plugin.run:
plugin.run()
assert len(init_called) == 1
assert len(apply_called) == 1
assert len(run_called) == 1
def test_plugin_with_widgets(self):
"""Test plugin with associated widgets."""
widget1 = ttk.TTkWidget()
widget2 = ttk.TTkWidget()
plugin_widget1 = TTkodePluginWidget(widget=widget1)
plugin_widget2 = TTkodePluginWidgetActivity(
widget=widget2,
activityName="Activity",
icon=ttk.TTkString("🔧")
)
plugin = TTkodePlugin(
name="WidgetPlugin",
widgets=[plugin_widget1, plugin_widget2]
)
assert len(plugin.widgets) == 2
assert plugin.widgets[0] is plugin_widget1
assert plugin.widgets[1] is plugin_widget2
def test_plugin_instances_list(self):
"""Test that plugins are registered in instances list."""
initial_count = len(TTkodePlugin.instances)
plugin1 = TTkodePlugin(name="Plugin1")
plugin2 = TTkodePlugin(name="Plugin2")
assert len(TTkodePlugin.instances) == initial_count + 2
assert plugin1 in TTkodePlugin.instances
assert plugin2 in TTkodePlugin.instances
def test_plugin_with_all_widget_types(self):
"""Test plugin containing different types of widgets."""
base_widget = ttk.TTkWidget()
activity_widget = ttk.TTkWidget()
panel_widget = ttk.TTkWidget()
widgets = [
TTkodePluginWidget(widget=base_widget),
TTkodePluginWidgetActivity(
widget=activity_widget,
activityName="MyActivity",
icon=ttk.TTkString("")
),
TTkodePluginWidgetPanel(
widget=panel_widget,
panelName="MyPanel"
)
]
plugin = TTkodePlugin(name="CompletePlugin", widgets=widgets)
assert len(plugin.widgets) == 3
assert isinstance(plugin.widgets[0], TTkodePluginWidget)
assert isinstance(plugin.widgets[1], TTkodePluginWidgetActivity)
assert isinstance(plugin.widgets[2], TTkodePluginWidgetPanel)
def test_plugin_callback_execution_order(self):
"""Test that plugin callbacks can be executed in order."""
execution_order = []
def init_cb():
execution_order.append('init')
def apply_cb():
execution_order.append('apply')
def run_cb():
execution_order.append('run')
plugin = TTkodePlugin(
name="OrderPlugin",
init=init_cb,
apply=apply_cb,
run=run_cb
)
# Simulate the plugin lifecycle
if plugin.init:
plugin.init()
if plugin.apply:
plugin.apply()
if plugin.run:
plugin.run()
assert execution_order == ['init', 'apply', 'run']

120
apps/ttkode/tests/test_proxy.py

@ -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

314
apps/ttkode/tests/test_search_file.py

@ -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

18
apps/ttkode/ttkode/app/ttkode.py

@ -35,6 +35,7 @@ from TermTk.TTkWidgets.tabwidget import _TTkNewTabWidgetDragData
from .about import About from .about import About
from .command_palette.command_palette import TTKode_CommandPalette from .command_palette.command_palette import TTKode_CommandPalette
from .activitybar import TTKodeActivityBar from .activitybar import TTKodeActivityBar
from .ttkode_terminal import TTkode_Terminal
class TTKodeWidget(): class TTKodeWidget():
def closeRequested(self, tab:ttk.TTkTabWidget, num:int): def closeRequested(self, tab:ttk.TTkTabWidget, num:int):
@ -265,6 +266,8 @@ class TTKode(ttk.TTkGridLayout):
helpMenu = appMenuBar.addMenu("&Help", alignment=ttk.TTkK.RIGHT_ALIGN) helpMenu = appMenuBar.addMenu("&Help", alignment=ttk.TTkK.RIGHT_ALIGN)
helpMenu.addMenu("About ...").menuButtonClicked.connect(_showAbout) helpMenu.addMenu("About ...").menuButtonClicked.connect(_showAbout)
helpMenu.addMenu("About ttk").menuButtonClicked.connect(_showAboutTTk) helpMenu.addMenu("About ttk").menuButtonClicked.connect(_showAboutTTk)
helpMenu.addSpacer()
helpMenu.addMenu("&KeypressView").menuButtonClicked.connect(self._keypressview)
fileTree = ttk.TTkFileTree(path='.', dragDropMode=ttk.TTkK.DragDropMode.AllowDrag, selectionMode=ttk.TTkK.SelectionMode.MultiSelection) fileTree = ttk.TTkFileTree(path='.', dragDropMode=ttk.TTkK.DragDropMode.AllowDrag, selectionMode=ttk.TTkK.SelectionMode.MultiSelection)
self._activityBar = TTKodeActivityBar() self._activityBar = TTKodeActivityBar()
@ -285,10 +288,7 @@ class TTKode(ttk.TTkGridLayout):
menuBar=bottomMenuBar) menuBar=bottomMenuBar)
_logViewer=ttk.TTkLogViewer() _logViewer=ttk.TTkLogViewer()
_terminal=ttk.TTkTerminal(visible=False) _terminal=TTkode_Terminal(visible=False)
_th = ttk.TTkTerminalHelper(term=_terminal)
_th.runShell()
self._panel.addWidget( self._panel.addWidget(
position=_Panel.Position.BOTTOM, position=_Panel.Position.BOTTOM,
@ -479,3 +479,13 @@ class TTKode(ttk.TTkGridLayout):
newEvt.setData(newData) newEvt.setData(newData)
return newEvt return newEvt
return evt return evt
@ttk.pyTTkSlot()
def _keypressview(self):
win = ttk.TTkWindow(
title="Mr Keypress 🔑🐁",
size=(70,7),
layout=(_l:=ttk.TTkGridLayout()),
flags=ttk.TTkK.WindowFlag.WindowMaximizeButtonHint|ttk.TTkK.WindowFlag.WindowCloseButtonHint)
_l.addWidget(ttk.TTkKeyPressView(maxHeight=3))
ttk.TTkHelper.overlay(None, win, 2, 2, toolWindow=True)

14
apps/ttkode/ttkode/app/ttkode_terminal.py

@ -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…
Cancel
Save