#!/usr/bin/python3 # -*- coding: utf-8 -*- # ----------------------------------------------------------------------- # This file is part of TISBackup # # TISBackup is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # TISBackup is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with TISBackup. If not, see . # # ----------------------------------------------------------------------- """ Test suite for libtisbackup.utils module. Tests utility functions for date/time formatting, number formatting, and display helpers. """ import datetime import os import tempfile from unittest.mock import Mock import pytest from libtisbackup.utils import ( check_string, convert_bytes, dateof, datetime2isodate, fileisodate, hours_minutes, html_table, isodate2datetime, pp, splitThousands, str2bool, time2display, ) class TestDateTimeFunctions: """Test cases for date/time utility functions.""" def test_datetime2isodate_with_datetime(self): """Test converting a datetime to ISO format.""" dt = datetime.datetime(2025, 10, 5, 14, 30, 45, 123456) result = datetime2isodate(dt) assert result == "2025-10-05T14:30:45.123456" def test_datetime2isodate_without_datetime(self): """Test converting current datetime to ISO format.""" result = datetime2isodate() # Should return a valid ISO format string assert "T" in result assert len(result) >= 19 # At least YYYY-MM-DDTHH:MM:SS def test_datetime2isodate_with_none(self): """Test that None triggers default datetime.now() behavior.""" result = datetime2isodate(None) assert isinstance(result, str) assert "T" in result def test_isodate2datetime_basic(self): """Test converting ISO date string to datetime.""" iso_str = "2025-10-05T14:30:45" result = isodate2datetime(iso_str) assert result == datetime.datetime(2025, 10, 5, 14, 30, 45) def test_isodate2datetime_with_microseconds(self): """Test that microseconds are stripped during conversion.""" iso_str = "2025-10-05T14:30:45.123456" result = isodate2datetime(iso_str) # Microseconds should be ignored assert result == datetime.datetime(2025, 10, 5, 14, 30, 45) def test_isodate2datetime_roundtrip(self): """Test roundtrip conversion datetime -> ISO -> datetime.""" original = datetime.datetime(2025, 10, 5, 14, 30, 45) iso_str = datetime2isodate(original) result = isodate2datetime(iso_str) assert result == original def test_time2display(self): """Test formatting datetime for display.""" dt = datetime.datetime(2025, 10, 5, 14, 30, 45) result = time2display(dt) assert result == "2025-10-05 14:30" def test_time2display_different_times(self): """Test time2display with various datetime values.""" test_cases = [ (datetime.datetime(2025, 1, 1, 0, 0, 0), "2025-01-01 00:00"), (datetime.datetime(2025, 12, 31, 23, 59, 59), "2025-12-31 23:59"), (datetime.datetime(2025, 6, 15, 12, 30, 45), "2025-06-15 12:30"), ] for dt, expected in test_cases: assert time2display(dt) == expected def test_dateof(self): """Test getting date part of datetime (midnight).""" dt = datetime.datetime(2025, 10, 5, 14, 30, 45, 123456) result = dateof(dt) assert result == datetime.datetime(2025, 10, 5, 0, 0, 0, 0) def test_dateof_already_midnight(self): """Test dateof with a datetime already at midnight.""" dt = datetime.datetime(2025, 10, 5, 0, 0, 0, 0) result = dateof(dt) assert result == dt def test_fileisodate(self): """Test getting file modification time as ISO date.""" with tempfile.NamedTemporaryFile(delete=False) as tmp: tmp_path = tmp.name tmp.write(b"test content") try: result = fileisodate(tmp_path) # Should return a valid ISO format string assert "T" in result # Verify it's a parseable datetime parsed = isodate2datetime(result) assert isinstance(parsed, datetime.datetime) finally: os.unlink(tmp_path) class TestHoursMinutes: """Test cases for hours_minutes function.""" def test_hours_minutes_whole_hours(self): """Test converting whole hours.""" assert hours_minutes(1.0) == "01:00" assert hours_minutes(5.0) == "05:00" assert hours_minutes(10.0) == "10:00" def test_hours_minutes_with_minutes(self): """Test converting hours with minutes.""" assert hours_minutes(1.5) == "01:30" assert hours_minutes(2.25) == "02:15" assert hours_minutes(3.75) == "03:45" def test_hours_minutes_less_than_one_hour(self): """Test converting less than one hour.""" assert hours_minutes(0.5) == "00:30" assert hours_minutes(0.25) == "00:15" assert hours_minutes(0.75) == "00:45" def test_hours_minutes_zero(self): """Test converting zero hours.""" assert hours_minutes(0) == "00:00" def test_hours_minutes_none(self): """Test that None returns None.""" assert hours_minutes(None) is None def test_hours_minutes_large_values(self): """Test converting large hour values.""" assert hours_minutes(24.0) == "24:00" assert hours_minutes(100.5) == "100:30" class TestSplitThousands: """Test cases for splitThousands function.""" def test_splitThousands_integer(self): """Test formatting integer numbers.""" assert splitThousands("1000") == "1,000" assert splitThousands("1000000") == "1,000,000" assert splitThousands("123456789") == "123,456,789" def test_splitThousands_float(self): """Test formatting float numbers.""" assert splitThousands("1000.50") == "1,000.50" assert splitThousands("1234567.89") == "1,234,567.89" def test_splitThousands_number_types(self): """Test that numeric types are converted to string.""" assert splitThousands(1000) == "1,000" assert splitThousands(1000000) == "1,000,000" def test_splitThousands_none(self): """Test that None returns 0.""" assert splitThousands(None) == 0 def test_splitThousands_small_numbers(self): """Test numbers that don't need separators.""" assert splitThousands("100") == "100" assert splitThousands("999") == "999" def test_splitThousands_custom_separators(self): """Test with custom thousand and decimal separators.""" assert splitThousands("1000.50", tSep=" ", dSep=".") == "1 000.50" assert splitThousands("1000,50", tSep=".", dSep=",") == "1.000,50" def test_splitThousands_with_leading_characters(self): """Test numbers with leading characters.""" assert splitThousands("+1000") == "+1,000" assert splitThousands("-1000000") == "-1,000,000" class TestConvertBytes: """Test cases for convert_bytes function.""" def test_convert_bytes_none(self): """Test that None returns None.""" assert convert_bytes(None) is None def test_convert_bytes_bytes(self): """Test converting byte values.""" assert convert_bytes(0) == "0.00b" assert convert_bytes(500) == "500.00b" assert convert_bytes(1023) == "1023.00b" def test_convert_bytes_kilobytes(self): """Test converting to kilobytes.""" assert convert_bytes(1024) == "1.00K" assert convert_bytes(1024 * 5) == "5.00K" assert convert_bytes(1024 * 100) == "100.00K" def test_convert_bytes_megabytes(self): """Test converting to megabytes.""" assert convert_bytes(1048576) == "1.00M" assert convert_bytes(1048576 * 10) == "10.00M" assert convert_bytes(1048576 * 500) == "500.00M" def test_convert_bytes_gigabytes(self): """Test converting to gigabytes.""" assert convert_bytes(1073741824) == "1.00G" assert convert_bytes(1073741824 * 5) == "5.00G" assert convert_bytes(1073741824 * 100) == "100.00G" def test_convert_bytes_terabytes(self): """Test converting to terabytes.""" assert convert_bytes(1099511627776) == "1.00T" assert convert_bytes(1099511627776 * 2) == "2.00T" assert convert_bytes(1099511627776 * 10) == "10.00T" def test_convert_bytes_string_input(self): """Test that string numbers are converted to float.""" assert convert_bytes("1024") == "1.00K" assert convert_bytes("1048576") == "1.00M" class TestCheckString: """Test cases for check_string function.""" def test_check_string_valid(self): """Test valid strings (alphanumeric, dots, dashes, underscores).""" # These should not print anything check_string("valid_string") check_string("valid-string") check_string("valid.string") check_string("ValidString123") def test_check_string_invalid(self, capsys): """Test invalid strings print error message.""" check_string("invalid string with spaces") captured = capsys.readouterr() assert "Invalid" in captured.out assert "invalid string with spaces" in captured.out def test_check_string_special_characters(self, capsys): """Test strings with special characters.""" check_string("invalid@string") captured = capsys.readouterr() assert "Invalid" in captured.out class TestStr2Bool: """Test cases for str2bool function.""" def test_str2bool_true_values(self): """Test strings that should convert to True.""" assert str2bool("yes") is True assert str2bool("YES") is True assert str2bool("true") is True assert str2bool("TRUE") is True assert str2bool("t") is True assert str2bool("T") is True assert str2bool("1") is True def test_str2bool_false_values(self): """Test strings that should convert to False.""" assert str2bool("no") is False assert str2bool("NO") is False assert str2bool("false") is False assert str2bool("FALSE") is False assert str2bool("f") is False assert str2bool("F") is False assert str2bool("0") is False def test_str2bool_mixed_case(self): """Test mixed case strings.""" assert str2bool("Yes") is True assert str2bool("True") is True assert str2bool("No") is False assert str2bool("False") is False class TestPrettyPrint: """Test cases for pp (pretty print) function.""" def test_pp_basic(self): """Test basic pretty printing of cursor results.""" # Mock cursor mock_cursor = Mock() mock_cursor.description = [("id", 10), ("name", 20)] mock_cursor.fetchall.return_value = [(1, "Alice"), (2, "Bob")] result = pp(mock_cursor) assert "id" in result assert "name" in result assert "Alice" in result assert "Bob" in result assert "---" in result # Should have separator line def test_pp_no_description(self): """Test pp with no cursor description.""" mock_cursor = Mock() mock_cursor.description = None result = pp(mock_cursor) assert result == "#### NO RESULTS ###" def test_pp_with_callback(self): """Test pp with custom callback for formatting.""" mock_cursor = Mock() mock_cursor.description = [("count", 10)] mock_cursor.fetchall.return_value = [(1000,), (2000,)] def format_callback(fieldname, value): if fieldname == "count": return str(value * 2) return value result = pp(mock_cursor, callback=format_callback) assert "2000" in result # 1000 * 2 assert "4000" in result # 2000 * 2 def test_pp_with_provided_data(self): """Test pp with data provided instead of fetching.""" mock_cursor = Mock() mock_cursor.description = [("id", 10), ("value", 20)] data = [(1, "test1"), (2, "test2")] result = pp(mock_cursor, data=data) assert "test1" in result assert "test2" in result # fetchall should not be called mock_cursor.fetchall.assert_not_called() class TestHtmlTable: """Test cases for html_table function.""" def test_html_table_basic(self): """Test basic HTML table generation.""" mock_cursor = Mock() mock_cursor.description = [("id",), ("name",)] mock_cursor.__iter__ = Mock(return_value=iter([("1", "Alice"), ("2", "Bob")])) result = html_table(mock_cursor) assert "" in result assert "id" in result assert "name" in result assert "1" in result assert "Alice" in result assert "2" in result assert "Bob" in result def test_html_table_with_callback(self): """Test HTML table with custom formatting callback.""" mock_cursor = Mock() mock_cursor.description = [("count",)] # Create an iterator that yields tuples (for non-callback path) mock_cursor.__iter__ = Mock(return_value=iter([("1000",), ("2000",)])) result = html_table(mock_cursor) assert "1" in result assert "test" in result def test_html_table_structure(self): """Test that HTML table has proper structure.""" mock_cursor = Mock() mock_cursor.description = [("col1",)] mock_cursor.__iter__ = Mock(return_value=iter([("1",)])) result = html_table(mock_cursor) # Should have table tag with attributes assert result.startswith("") assert "cellpadding=2" in result assert "cellspacing=0" in result class TestUtilsIntegration: """Integration tests for utilities working together.""" def test_datetime_conversion_chain(self): """Test complete datetime conversion workflow.""" # Create a datetime original = datetime.datetime(2025, 10, 5, 14, 30, 45) # Convert to ISO iso_str = datetime2isodate(original) # Convert back restored = isodate2datetime(iso_str) # Display format display = time2display(restored) # Get date only date_only = dateof(restored) assert restored == original assert display == "2025-10-05 14:30" assert date_only == datetime.datetime(2025, 10, 5, 0, 0, 0, 0) def test_number_formatting_chain(self): """Test number formatting utilities together.""" # Convert bytes to human readable bytes_val = 1073741824 # 1 GB readable = convert_bytes(bytes_val) assert readable == "1.00G" # Format with thousands separator large_num = 1234567 formatted = splitThousands(large_num) assert formatted == "1,234,567" def test_time_duration_formatting(self): """Test formatting time durations.""" # Different durations in hours durations = [0.5, 1.25, 2.75, 10.5] expected = ["00:30", "01:15", "02:45", "10:30"] for duration, expected_format in zip(durations, expected): assert hours_minutes(duration) == expected_format