#!/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