You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

521 lines
20 KiB

#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2025 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, os
import pytest
from unittest.mock import Mock
sys.path.append(os.path.join(sys.path[0],'../../../libs/pyTermTk'))
import TermTk as ttk
class TestTTkTableModelList:
"""Test cases for TTkTableModelList class"""
def setup_method(self):
"""Set up test fixtures before each test method."""
self.test_data = [
['Alice', 25, 'Engineer'],
['Bob', 30, 'Designer'],
['Charlie', 35, 'Manager']
]
self.header = ['Name', 'Age', 'Role']
self.indexes = ['Row1', 'Row2', 'Row3']
def test_init_default(self):
"""Test default initialization"""
model = ttk.TTkTableModelList()
assert model.rowCount() == 0
assert model.columnCount() == 0
assert model.data(0, 0) is None
def test_init_with_data(self):
"""Test initialization with data"""
model = ttk.TTkTableModelList(data=self.test_data)
assert model.rowCount() == 3
assert model.columnCount() == 3
assert model.data(0, 0) == 'Alice'
assert model.data(1, 1) == 30
assert model.data(2, 2) == 'Manager'
def test_init_with_headers_and_indexes(self):
"""Test initialization with headers and indexes"""
model = ttk.TTkTableModelList(
data=self.test_data,
header=self.header,
indexes=self.indexes
)
assert model.rowCount() == 3
assert model.columnCount() == 3
# Check header data
assert model.headerData(0, ttk.TTkK.HORIZONTAL) == 'Name'
assert model.headerData(1, ttk.TTkK.HORIZONTAL) == 'Age'
assert model.headerData(2, ttk.TTkK.HORIZONTAL) == 'Role'
# Check index data
assert model.headerData(0, ttk.TTkK.VERTICAL) == 'Row1'
assert model.headerData(1, ttk.TTkK.VERTICAL) == 'Row2'
assert model.headerData(2, ttk.TTkK.VERTICAL) == 'Row3'
def test_modelList_getter_setter(self):
"""Test modelList getter and setter"""
model = ttk.TTkTableModelList(data=self.test_data)
# Test getter
retrieved_data = model.modelList()
assert retrieved_data == self.test_data
# Test setter with new data
new_data = [['X', 1], ['Y', 2]]
model.setModelList(new_data)
assert model.modelList() == new_data
assert model.rowCount() == 2
assert model.columnCount() == 2
# Test setter with same data (should not trigger change)
model.setModelList(new_data) # Same data, no change expected
assert model.modelList() == new_data
def test_data_access(self):
"""Test data access methods"""
model = ttk.TTkTableModelList(data=self.test_data)
# Test data method
assert model.data(0, 0) == 'Alice'
assert model.data(1, 1) == 30
assert model.data(2, 2) == 'Manager'
# Test setData method
model.setData(0, 0, 'Alicia')
assert model.data(0, 0) == 'Alicia'
model.setData(1, 1, 31)
assert model.data(1, 1) == 31
def test_index_method(self):
"""Test index method returns correct TTkModelIndex"""
model = ttk.TTkTableModelList(data=self.test_data)
index = model.index(0, 0)
assert index.row() == 0
assert index.col() == 0
assert index.data() == 'Alice'
index = model.index(1, 2)
assert index.row() == 1
assert index.col() == 2
assert index.data() == 'Designer'
# Test setData through index
index.setData('Senior Designer')
assert model.data(1, 2) == 'Senior Designer'
def test_header_data_no_headers(self):
"""Test headerData when no headers are provided"""
model = ttk.TTkTableModelList(data=self.test_data)
# Should return default behavior (from parent class)
h_result = model.headerData(0, ttk.TTkK.HORIZONTAL)
v_result = model.headerData(0, ttk.TTkK.VERTICAL)
# These should return whatever the parent implementation returns
# (likely default column/row numbers or empty strings)
assert h_result is not None or h_result is None # Accept any default
assert v_result is not None or v_result is None # Accept any default
def test_flags(self):
"""Test flags method returns correct item flags"""
model = ttk.TTkTableModelList(data=self.test_data)
flags = model.flags(0, 0)
expected_flags = (
ttk.TTkK.ItemFlag.ItemIsEnabled |
ttk.TTkK.ItemFlag.ItemIsEditable |
ttk.TTkK.ItemFlag.ItemIsSelectable
)
assert flags == expected_flags
def test_sort_by_column(self):
"""Test sorting functionality"""
# Test data with mixed types for sorting edge cases
sort_data = [
['Charlie', 35, 'Manager'],
['Alice', 25, 'Engineer'],
['Bob', 30, 'Designer']
]
model = ttk.TTkTableModelList(data=sort_data)
# Sort by first column (names) - ascending
model.sort(0, ttk.TTkK.SortOrder.AscendingOrder)
sorted_data = model.modelList()
assert sorted_data[0][0] == 'Alice'
assert sorted_data[1][0] == 'Bob'
assert sorted_data[2][0] == 'Charlie'
# Sort by first column (names) - descending
model.sort(0, ttk.TTkK.SortOrder.DescendingOrder)
sorted_data = model.modelList()
assert sorted_data[0][0] == 'Charlie'
assert sorted_data[1][0] == 'Bob'
assert sorted_data[2][0] == 'Alice'
# Sort by second column (ages) - ascending
model.sort(1, ttk.TTkK.SortOrder.AscendingOrder)
sorted_data = model.modelList()
assert sorted_data[0][1] == 25 # Alice
assert sorted_data[1][1] == 30 # Bob
assert sorted_data[2][1] == 35 # Charlie
# Reset to original order
model.sort(-1, ttk.TTkK.SortOrder.AscendingOrder)
reset_data = model.modelList()
assert reset_data == sort_data # Should be back to original
def test_sort_mixed_types(self):
"""Test sorting with mixed data types (fallback to string sorting)"""
mixed_data = [
['Item', 100],
['Item', 'abc'],
['Item', 50]
]
model = ttk.TTkTableModelList(data=mixed_data)
# This should trigger the TypeError exception and fall back to string sorting
model.sort(1, ttk.TTkK.SortOrder.AscendingOrder)
# Verify it doesn't crash and data is still accessible
assert model.rowCount() == 3
assert model.columnCount() == 2
def test_insert_rows(self):
"""Test insertRows method"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data]) # Deep copy
original_count = model.rowCount()
# Insert 2 rows at position 1
result = model.insertRows(1, 2)
assert result == True
assert model.rowCount() == original_count + 2
# Check that empty string values were inserted
assert model.data(1, 0) == ''
assert model.data(1, 1) == ''
assert model.data(1, 2) == ''
assert model.data(2, 0) == ''
assert model.data(2, 1) == ''
assert model.data(2, 2) == ''
# Check that existing data was shifted
assert model.data(3, 0) == 'Bob' # Was at row 1, now at row 3
def test_insert_columns(self):
"""Test insertColumns method"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data]) # Deep copy
original_column_count = model.columnCount()
# Insert 2 columns at position 1
result = model.insertColumns(1, 2)
assert result == True
assert model.columnCount() == original_column_count + 2
# Check that empty string values were inserted at the right position
assert model.data(0, 1) == '' # New column 1
assert model.data(0, 2) == '' # New column 2
assert model.data(0, 3) == 25 # Original column 1 shifted to position 3
# Check all rows have the new columns
for row in range(model.rowCount()):
assert model.data(row, 1) == ''
assert model.data(row, 2) == ''
def test_remove_columns(self):
"""Test removeColumns method"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data]) # Deep copy
original_column_count = model.columnCount()
# Remove 1 column at position 1 (Age column)
result = model.removeColumns(1, 1)
assert result == True
assert model.columnCount() == original_column_count - 1
# Check that the middle column was removed
assert model.data(0, 0) == 'Alice' # Name still there
assert model.data(0, 1) == 'Engineer' # Role shifted to position 1
assert model.data(1, 0) == 'Bob'
assert model.data(1, 1) == 'Designer'
def test_remove_rows(self):
"""Test removeRows method"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data]) # Deep copy
original_row_count = model.rowCount()
# Remove 1 row at position 1 (Bob's row)
result = model.removeRows(1, 1)
assert result == True
assert model.rowCount() == original_row_count - 1
# Check that the middle row was removed and data shifted
assert model.data(0, 0) == 'Alice' # First row unchanged
assert model.data(1, 0) == 'Charlie' # Charlie moved to position 1
def test_remove_rows_boundary_conditions(self):
"""Test removeRows method with boundary conditions and invalid ranges"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data]) # Deep copy
original_row_count = model.rowCount()
# Test removing rows at the edge (last row)
result = model.removeRows(2, 1) # Remove Charlie's row (index 2)
assert result == True
assert model.rowCount() == original_row_count - 1
assert model.data(0, 0) == 'Alice'
assert model.data(1, 0) == 'Bob'
# Charlie should be gone
assert model.data(2, 0) is None
# Reset for next test
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
# Test removing rows starting beyond valid range
try:
result = model.removeRows(5, 1) # Start beyond data bounds
# Should either return False or handle gracefully
# The behavior depends on implementation - some might allow it
except IndexError:
pass # This is acceptable behavior
# Test removing rows with count that goes beyond data bounds
result = model.removeRows(1, 10) # Remove more rows than exist
assert result == True
# Should remove rows 1 and 2 (Bob and Charlie), leaving only Alice
assert model.rowCount() == 1
assert model.data(0, 0) == 'Alice'
# Reset for next test
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
# Test removing rows with negative start index
try:
result = model.removeRows(-1, 1)
# Behavior may vary - could be handled gracefully or raise error
except (IndexError, ValueError):
pass # Expected for invalid index
# Test removing zero rows
result = model.removeRows(1, 0)
assert result == True
assert model.rowCount() == original_row_count # Should be unchanged
def test_remove_columns_boundary_conditions(self):
"""Test removeColumns method with boundary conditions and invalid ranges"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data]) # Deep copy
original_column_count = model.columnCount()
# Test removing columns at the edge (last column)
result = model.removeColumns(2, 1) # Remove Role column (index 2)
assert result == True
assert model.columnCount() == original_column_count - 1
assert model.data(0, 0) == 'Alice' # Name still there
assert model.data(0, 1) == 25 # Age still there
assert model.data(0, 2) is None # Role should be gone
# Reset for next test
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
# Test removing columns starting beyond valid range
try:
result = model.removeColumns(5, 1) # Start beyond data bounds
# Should either return False or handle gracefully
except IndexError:
pass # This is acceptable behavior
# Test removing columns with count that goes beyond data bounds
result = model.removeColumns(1, 10) # Remove more columns than exist
assert result == True
# Should remove columns 1 and 2 (Age and Role), leaving only Name
assert model.columnCount() == 1
assert model.data(0, 0) == 'Alice'
assert model.data(0, 1) is None # Age should be gone
# Reset for next test
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
# Test removing columns with negative start index
try:
result = model.removeColumns(-1, 1)
# Behavior may vary - could be handled gracefully or raise error
except (IndexError, ValueError):
pass # Expected for invalid index
# Test removing zero columns
result = model.removeColumns(1, 0)
assert result == True
assert model.columnCount() == original_column_count # Should be unchanged
def test_remove_operations_on_empty_model(self):
"""Test remove operations on empty model"""
model = ttk.TTkTableModelList(data=[])
# Test removing rows from empty model
try:
result = model.removeRows(0, 1)
# Should handle gracefully or return False
except IndexError:
pass # Acceptable behavior
# Test removing columns from empty model
try:
result = model.removeColumns(0, 1)
# Should handle gracefully or return False
except IndexError:
pass # Acceptable behavior
def test_remove_operations_on_single_cell_model(self):
"""Test remove operations on single cell model"""
model = ttk.TTkTableModelList(data=[['single']])
# Remove the only row
result = model.removeRows(0, 1)
assert result == True
assert model.rowCount() == 0
assert model.columnCount() == 0 # Column structure might remain
# Reset for column test
model = ttk.TTkTableModelList(data=[['single']])
# Remove the only column
result = model.removeColumns(0, 1)
assert result == True
assert model.columnCount() == 0
assert model.rowCount() == 1 # Row structure might remain
def test_remove_all_rows(self):
"""Test removing all rows from model"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
# Remove all rows at once
result = model.removeRows(0, model.rowCount())
assert result == True
assert model.rowCount() == 0
def test_remove_all_columns(self):
"""Test removing all columns from model"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
# Remove all columns at once
result = model.removeColumns(0, model.columnCount())
assert result == True
assert model.columnCount() == 0
def test_remove_operations_signal_emission_boundary_cases(self):
"""Test signal emission for boundary cases in remove operations"""
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
# Mock the signal to track emissions
signal_mock = Mock()
@ttk.pyTTkSlot(tuple[int,int], tuple[int,int])
def _mock_signal(pos: tuple, size: tuple):
signal_mock(pos, size)
model.dataChanged.connect(_mock_signal)
# Remove last row
model.removeRows(2, 1)
# Should emit signal for the removal
assert signal_mock.call_count >= 1
# Reset mock and model
signal_mock.reset_mock()
model = ttk.TTkTableModelList(data=[row[:] for row in self.test_data])
model.dataChanged.connect(_mock_signal)
# Remove last column
model.removeColumns(2, 1)
# Should emit signal for the removal
assert signal_mock.call_count >= 1
def test_remove_operations_preserve_data_integrity(self):
"""Test that remove operations preserve data integrity"""
original_data = [
['A', 'B', 'C', 'D'],
['E', 'F', 'G', 'H'],
['I', 'J', 'K', 'L'],
['M', 'N', 'O', 'P']
]
model = ttk.TTkTableModelList(data=[row[:] for row in original_data])
# Remove middle rows (1 and 2)
model.removeRows(1, 2)
# Verify remaining data is correct
assert model.rowCount() == 2
assert model.data(0, 0) == 'A' # First row unchanged
assert model.data(1, 0) == 'M' # Last row shifted up
# Reset and test column removal
model = ttk.TTkTableModelList(data=[row[:] for row in original_data])
# Remove middle columns (1 and 2)
model.removeColumns(1, 2)
# Verify remaining data is correct
assert model.columnCount() == 2
assert model.data(0, 0) == 'A' # First column unchanged
assert model.data(0, 1) == 'D' # Last column shifted left
assert model.data(1, 0) == 'E'
assert model.data(1, 1) == 'H'
def test_integration_with_table_widget():
"""Integration test - verify TTkTableModelList works with table widgets"""
# This test ensures the model integrates properly with the UI components
test_data = [
['Item 1', 'Value 1'],
['Item 2', 'Value 2']
]
model = ttk.TTkTableModelList(data=test_data, header=['Name', 'Value'])
# These are the basic requirements for table model integration
assert model.rowCount() > 0
assert model.columnCount() > 0
assert callable(model.data)
assert callable(model.setData)
assert callable(model.index)
assert callable(model.flags)
assert callable(model.headerData)
# Test that all expected methods return appropriate types
assert isinstance(model.rowCount(), int)
assert isinstance(model.columnCount(), int)
assert isinstance(model.data(0, 0), (str, int, float, type(None)))
assert isinstance(model.flags(0, 0), ttk.TTkK.ItemFlag) # Flags are integer bitmasks
if __name__ == '__main__':
pytest.main([__file__])