#!/usr/bin/env python3 # MIT License # # Copyright (c) 2025 Eugenio Parodi # # 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__])