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.
404 lines
15 KiB
404 lines
15 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 os |
|
import sys |
|
import csv |
|
import tempfile |
|
import io |
|
from unittest.mock import Mock |
|
import pytest |
|
|
|
sys.path.append(os.path.join(sys.path[0],'../../../libs/pyTermTk')) |
|
|
|
import TermTk as ttk |
|
|
|
class TestTTkTableModelCSV: |
|
"""Test cases for TTkTableModelCSV class""" |
|
|
|
def setup_method(self): |
|
"""Set up test fixtures before each test method.""" |
|
# Sample CSV content with headers |
|
self.csv_with_headers = "Name,Age,Role\nAlice,25,Engineer\nBob,30,Designer\nCharlie,35,Manager" |
|
|
|
# Sample CSV content without headers |
|
self.csv_without_headers = "Alice,25,Engineer\nBob,30,Designer\nCharlie,35,Manager" |
|
|
|
# CSV with index column (sequential numbers starting from 1) |
|
self.csv_with_index = "1,Alice,25,Engineer\n2,Bob,30,Designer\n3,Charlie,35,Manager" |
|
|
|
# CSV with headers and index |
|
self.csv_with_headers_and_index = "ID,Name,Age,Role\n1,Alice,25,Engineer\n2,Bob,30,Designer\n3,Charlie,35,Manager" |
|
|
|
# CSV with non-sequential index (should not be detected as index) |
|
self.csv_with_non_sequential_index = "10,Alice,25,Engineer\n20,Bob,30,Designer\n30,Charlie,35,Manager" |
|
|
|
def test_init_with_filename(self): |
|
"""Test initialization with CSV filename""" |
|
# Create temporary CSV file |
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as tmp: |
|
tmp.write(self.csv_with_headers) |
|
tmp_path = tmp.name |
|
|
|
try: |
|
model = ttk.TTkTableModelCSV(filename=tmp_path) |
|
|
|
assert model.rowCount() == 3 |
|
assert model.columnCount() == 3 |
|
assert model.data(0, 0) == 'Alice' |
|
assert model.data(0, 1) == '25' |
|
assert model.data(0, 2) == 'Engineer' |
|
|
|
# Check headers |
|
assert model.headerData(0, ttk.TTkK.HORIZONTAL) == 'Name' |
|
assert model.headerData(1, ttk.TTkK.HORIZONTAL) == 'Age' |
|
assert model.headerData(2, ttk.TTkK.HORIZONTAL) == 'Role' |
|
|
|
finally: |
|
os.unlink(tmp_path) |
|
|
|
def test_init_with_file_descriptor(self): |
|
"""Test initialization with file descriptor""" |
|
csv_fd = io.StringIO(self.csv_with_headers) |
|
|
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
assert model.rowCount() == 3 |
|
assert model.columnCount() == 3 |
|
assert model.data(0, 0) == 'Alice' |
|
assert model.data(1, 0) == 'Bob' |
|
assert model.data(2, 0) == 'Charlie' |
|
|
|
def test_init_with_no_parameters(self): |
|
"""Test initialization with no parameters (should create empty model)""" |
|
model = ttk.TTkTableModelCSV() |
|
|
|
assert model.rowCount() == 1 |
|
assert model.columnCount() == 1 |
|
assert model.data(0, 0) == '' |
|
|
|
def test_csv_import_with_headers(self): |
|
"""Test CSV import when headers are detected""" |
|
csv_fd = io.StringIO(self.csv_with_headers) |
|
|
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
# Check data |
|
assert model.rowCount() == 3 |
|
assert model.columnCount() == 3 |
|
|
|
# Check that headers were extracted |
|
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 that first row is actual data, not headers |
|
assert model.data(0, 0) == 'Alice' |
|
assert model.data(0, 1) == '25' |
|
|
|
def test_csv_import_without_headers(self): |
|
"""Test CSV import when no headers are detected""" |
|
csv_fd = io.StringIO(self.csv_without_headers) |
|
|
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
assert model.rowCount() == 3 |
|
assert model.columnCount() == 3 |
|
|
|
# Should use default header behavior (from parent class) |
|
# First row should be data, not treated as headers |
|
assert model.data(0, 0) == 'Alice' |
|
assert model.data(0, 1) == '25' |
|
assert model.data(0, 2) == 'Engineer' |
|
|
|
def test_csv_import_with_index_column(self): |
|
"""Test CSV import with sequential index column detection""" |
|
csv_fd = io.StringIO(self.csv_with_index) |
|
|
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
# Index column should be detected and removed from data |
|
assert model.rowCount() == 3 |
|
assert model.columnCount() == 3 # Index column removed |
|
|
|
# Check data (should start from second column of original CSV) |
|
assert model.data(0, 0) == 'Alice' |
|
assert model.data(0, 1) == '25' |
|
assert model.data(0, 2) == 'Engineer' |
|
|
|
# Check vertical headers (indexes) |
|
assert model.headerData(0, ttk.TTkK.VERTICAL) == '1' |
|
assert model.headerData(1, ttk.TTkK.VERTICAL) == '2' |
|
assert model.headerData(2, ttk.TTkK.VERTICAL) == '3' |
|
|
|
def test_csv_import_with_headers_and_index(self): |
|
"""Test CSV import with both headers and index column""" |
|
csv_fd = io.StringIO(self.csv_with_headers_and_index) |
|
|
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
assert model.rowCount() == 3 |
|
assert model.columnCount() == 3 # ID column removed |
|
|
|
# Check that both headers and indexes were properly extracted |
|
assert model.headerData(0, ttk.TTkK.HORIZONTAL) == 'Name' |
|
assert model.headerData(1, ttk.TTkK.HORIZONTAL) == 'Age' |
|
assert model.headerData(2, ttk.TTkK.HORIZONTAL) == 'Role' |
|
|
|
assert model.headerData(0, ttk.TTkK.VERTICAL) == '1' |
|
assert model.headerData(1, ttk.TTkK.VERTICAL) == '2' |
|
assert model.headerData(2, ttk.TTkK.VERTICAL) == '3' |
|
|
|
# Check data |
|
assert model.data(0, 0) == 'Alice' |
|
assert model.data(1, 0) == 'Bob' |
|
assert model.data(2, 0) == 'Charlie' |
|
|
|
def test_check_index_column_sequential(self): |
|
"""Test _checkIndexColumn method with sequential numbers""" |
|
csv_fd = io.StringIO(self.csv_with_index) |
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
# Create test data similar to what _csvImport would produce |
|
test_data = [['1', 'Alice', '25'], ['2', 'Bob', '30'], ['3', 'Charlie', '35']] |
|
|
|
# Should detect sequential index starting from 1 |
|
result = model._checkIndexColumn(test_data) |
|
assert result == True |
|
|
|
def test_check_index_column_non_sequential(self): |
|
"""Test _checkIndexColumn method with non-sequential numbers""" |
|
csv_fd = io.StringIO(self.csv_with_non_sequential_index) |
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
# Create test data with non-sequential numbers |
|
test_data = [['10', 'Alice', '25'], ['20', 'Bob', '30'], ['30', 'Charlie', '35']] |
|
|
|
# Should not detect as index column |
|
result = model._checkIndexColumn(test_data) |
|
assert result == False |
|
|
|
def test_check_index_column_non_numeric(self): |
|
"""Test _checkIndexColumn method with non-numeric first column""" |
|
model = ttk.TTkTableModelCSV() |
|
|
|
# Create test data with non-numeric first column |
|
test_data = [['Alice', '25'], ['Bob', '30'], ['Charlie', '35']] |
|
|
|
# Should not detect as index column |
|
result = model._checkIndexColumn(test_data) |
|
assert result == False |
|
|
|
def test_check_index_column_empty_data(self): |
|
"""Test _checkIndexColumn method with empty data""" |
|
model = ttk.TTkTableModelCSV() |
|
|
|
# Empty data should not crash |
|
result = model._checkIndexColumn([]) |
|
assert result == False |
|
|
|
def test_check_index_column_starting_from_zero(self): |
|
"""Test _checkIndexColumn with sequential numbers starting from 0""" |
|
model = ttk.TTkTableModelCSV() |
|
|
|
test_data = [['0', 'Alice'], ['1', 'Bob'], ['2', 'Charlie']] |
|
result = model._checkIndexColumn(test_data) |
|
assert result == True |
|
|
|
def test_csv_import_different_delimiters(self): |
|
"""Test CSV import with different delimiters""" |
|
# Test semicolon delimiter |
|
csv_semicolon = "Alice;25;Engineer\nBob;30;Designer" |
|
csv_fd = io.StringIO(csv_semicolon) |
|
|
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
# Should handle different delimiters automatically |
|
assert model.rowCount() >= 1 |
|
assert model.columnCount() >= 1 |
|
|
|
def test_csv_import_with_quotes(self): |
|
"""Test CSV import with quoted values""" |
|
csv_quoted = 'Name,Description\n"Alice Johnson","Senior Engineer"\n"Bob Smith","UI Designer"' |
|
csv_fd = io.StringIO(csv_quoted) |
|
|
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
assert model.data(1, 0) == 'Alice Johnson' |
|
assert model.data(1, 1) == 'Senior Engineer' |
|
assert model.data(2, 0) == 'Bob Smith' |
|
|
|
def test_csv_import_with_empty_cells(self): |
|
"""Test CSV import with empty cells""" |
|
csv_empty = "Name,Age,Role\nAlice,,Engineer\n,30,Designer\nCharlie,35," |
|
csv_fd = io.StringIO(csv_empty) |
|
|
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
assert model.data(1, 0) == 'Alice' |
|
assert model.data(1, 1) == '' # Empty age |
|
assert model.data(2, 0) == '' # Empty name |
|
assert model.data(3, 2) == '' # Empty role |
|
|
|
def test_csv_import_single_row(self): |
|
"""Test CSV import with single data row""" |
|
csv_single = "Name,Age\nAlice,25" |
|
csv_fd = io.StringIO(csv_single) |
|
|
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
assert model.rowCount() == 1 |
|
assert model.columnCount() == 2 |
|
assert model.data(0, 0) == 'Alice' |
|
assert model.data(0, 1) == '25' |
|
|
|
def test_csv_import_single_column(self): |
|
"""Test CSV import with single column""" |
|
csv_single_col = "Names\nAlice\nBob\nCharlie" |
|
csv_fd = io.StringIO(csv_single_col) |
|
|
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
assert model.rowCount() == 4 |
|
assert model.columnCount() == 1 |
|
assert model.data(1, 0) == 'Alice' |
|
assert model.data(2, 0) == 'Bob' |
|
assert model.data(3, 0) == 'Charlie' |
|
|
|
def test_file_not_found_error(self): |
|
"""Test error handling for non-existent file""" |
|
with pytest.raises(FileNotFoundError): |
|
ttk.TTkTableModelCSV(filename='/nonexistent/path/file.csv') |
|
|
|
def test_invalid_csv_format(self): |
|
"""Test handling of malformed CSV""" |
|
# This should still work as csv.reader is quite forgiving |
|
malformed_csv = 'Name,Age\n"Alice,25\nBob,30' |
|
csv_fd = io.StringIO(malformed_csv) |
|
|
|
# Should not raise exception, csv.reader handles it |
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
assert model.rowCount() >= 1 |
|
|
|
def test_inherited_functionality(self): |
|
"""Test that inherited TTkTableModelList functionality works""" |
|
csv_fd = io.StringIO(self.csv_with_headers) |
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
# Test inherited methods |
|
assert isinstance(model.flags(0, 0), ttk.TTkK.ItemFlag) |
|
|
|
# Test setData (inherited) |
|
original_value = model.data(0, 0) |
|
model.setData(0, 0, 'Alicia') |
|
assert model.data(0, 0) == 'Alicia' |
|
|
|
# Test index method (inherited) |
|
index = model.index(0, 1) |
|
assert index.row() == 0 |
|
assert index.col() == 1 |
|
|
|
def test_csv_import_large_file(self): |
|
"""Test CSV import with larger dataset""" |
|
# Generate larger CSV content |
|
large_csv_lines = ['Name,ID,Value'] |
|
for i in range(100): |
|
large_csv_lines.append(f'User{i},{i},Value{i}') |
|
large_csv = '\n'.join(large_csv_lines) |
|
|
|
csv_fd = io.StringIO(large_csv) |
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
assert model.rowCount() == 100 |
|
assert model.columnCount() == 3 |
|
assert model.data(0, 0) == 'User0' |
|
assert model.data(99, 0) == 'User99' |
|
|
|
def test_csv_with_unicode_characters(self): |
|
"""Test CSV import with unicode characters""" |
|
unicode_csv = "Name,City\nAlice,Москва\nBob,北京\nCharlie,São Paulo" |
|
csv_fd = io.StringIO(unicode_csv) |
|
|
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
assert model.data(1, 1) == 'Москва' |
|
assert model.data(2, 1) == '北京' |
|
assert model.data(3, 1) == 'São Paulo' |
|
|
|
def test_csv_import_preserves_file_position(self): |
|
"""Test that _csvImport properly handles file position""" |
|
csv_fd = io.StringIO(self.csv_with_headers) |
|
|
|
# Read some content first |
|
first_line = csv_fd.readline() |
|
assert 'Name,Age,Role' in first_line |
|
|
|
# Reset and use with model |
|
csv_fd.seek(0) |
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
# Should work correctly despite previous read |
|
assert model.rowCount() == 3 |
|
assert model.data(0, 0) == 'Alice' |
|
|
|
def test_sort_functionality_inherited(self): |
|
"""Test that sorting functionality from parent class works""" |
|
csv_fd = io.StringIO(self.csv_with_headers) |
|
model = ttk.TTkTableModelCSV(fd=csv_fd) |
|
|
|
# Test sorting by name column |
|
model.sort(0, ttk.TTkK.SortOrder.DescendingOrder) |
|
|
|
# Should be sorted: Charlie, Bob, Alice |
|
assert model.data(0, 0) == 'Charlie' |
|
assert model.data(1, 0) == 'Bob' |
|
assert model.data(2, 0) == 'Alice' |
|
|
|
|
|
def test_integration_with_table_widget(): |
|
"""Integration test - verify TTkTableModelCSV works with table widgets""" |
|
csv_content = "Product,Price,Stock\nLaptop,999.99,50\nMouse,25.99,100\nKeyboard,75.50,75" |
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as tmp: |
|
tmp.write(csv_content) |
|
tmp_path = tmp.name |
|
|
|
try: |
|
model = ttk.TTkTableModelCSV(filename=tmp_path) |
|
table = ttk.TTkTable(tableModel=model) |
|
|
|
# Test integration |
|
assert table.model() == model |
|
assert model.rowCount() == 3 |
|
assert model.columnCount() == 3 |
|
|
|
# Test that table can access model data |
|
assert model.data(0, 0) == 'Laptop' |
|
assert model.headerData(0, ttk.TTkK.HORIZONTAL) == 'Product' |
|
|
|
finally: |
|
os.unlink(tmp_path) |
|
|
|
|
|
if __name__ == '__main__': |
|
pytest.main([__file__]) |