mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
466 lines
16 KiB
Python
466 lines
16 KiB
Python
"""
|
|
Tests for LeapYear Tool
|
|
|
|
Tests the fast leap year calculations using Ben Joffe's algorithm.
|
|
These tests verify the correctness of leap year detection for both
|
|
Gregorian and Julian calendars, as well as date validation and utilities.
|
|
"""
|
|
|
|
import pytest
|
|
import threading
|
|
from unittest.mock import patch
|
|
import time
|
|
|
|
from nodupe.tools.leap_year import LeapYearTool
|
|
|
|
|
|
class TestLeapYearTool:
|
|
"""Test suite for LeapYearTool."""
|
|
|
|
def test_tool_metadata(self):
|
|
"""Test that tool metadata is correctly defined."""
|
|
tool = LeapYearTool()
|
|
metadata = tool.metadata
|
|
|
|
assert metadata.name == "LeapYear"
|
|
assert metadata.version == "1.0.0"
|
|
assert "Ben Joffe" in metadata.description
|
|
assert "algorithm" in metadata.description
|
|
assert "date" in metadata.tags
|
|
assert "leap-year" in metadata.tags
|
|
|
|
def test_initialization_defaults(self):
|
|
"""Test tool initialization with default values."""
|
|
tool = LeapYearTool()
|
|
|
|
assert tool.calendar == "gregorian"
|
|
assert tool.enable_cache is True
|
|
assert tool.cache_size == 10000
|
|
assert tool.min_year == 1
|
|
assert tool.max_year == 9999
|
|
|
|
def test_initialization_custom_values(self):
|
|
"""Test tool initialization with custom values."""
|
|
tool = LeapYearTool(
|
|
calendar="julian",
|
|
enable_cache=False,
|
|
cache_size=5000,
|
|
min_year=1000,
|
|
max_year=3000
|
|
)
|
|
|
|
assert tool.calendar == "julian"
|
|
assert tool.enable_cache is False
|
|
assert tool.cache_size == 5000
|
|
assert tool.min_year == 1000
|
|
assert tool.max_year == 3000
|
|
|
|
def test_invalid_calendar(self):
|
|
"""Test that invalid calendar raises ValueError."""
|
|
with pytest.raises(ValueError, match="Unsupported calendar"):
|
|
LeapYearTool(calendar="invalid")
|
|
|
|
def test_year_validation(self):
|
|
"""Test year validation."""
|
|
tool = LeapYearTool(min_year=1900, max_year=2100)
|
|
|
|
# Valid years
|
|
tool._validate_year(1900)
|
|
tool._validate_year(2000)
|
|
tool._validate_year(2100)
|
|
|
|
# Invalid years
|
|
with pytest.raises(ValueError, match="out of range"):
|
|
tool._validate_year(1899)
|
|
|
|
with pytest.raises(ValueError, match="out of range"):
|
|
tool._validate_year(2101)
|
|
|
|
with pytest.raises(TypeError, match="must be an integer"):
|
|
tool._validate_year("2000")
|
|
|
|
# ---- Gregorian leap year tests ----
|
|
def test_gregorian_leap_years(self):
|
|
"""Test known Gregorian leap years."""
|
|
tool = LeapYearTool(calendar="gregorian")
|
|
|
|
# Known leap years
|
|
leap_years = [2000, 2004, 2008, 2012, 2016, 2020, 2024, 2028, 2032]
|
|
for year in leap_years:
|
|
assert tool.is_leap_year(year), f"{year} should be a leap year"
|
|
|
|
# Known non-leap years
|
|
non_leap_years = [1900, 2100, 2200, 2300, 2001, 2002, 2003, 2005]
|
|
for year in non_leap_years:
|
|
assert not tool.is_leap_year(year), f"{year} should not be a leap year"
|
|
|
|
def test_gregorian_century_years(self):
|
|
"""Test Gregorian century year rules."""
|
|
tool = LeapYearTool(calendar="gregorian")
|
|
|
|
# Divisible by 400 (leap years)
|
|
assert tool.is_leap_year(1600)
|
|
assert tool.is_leap_year(2000)
|
|
assert tool.is_leap_year(2400)
|
|
|
|
# Divisible by 100 but not 400 (not leap years)
|
|
assert not tool.is_leap_year(1700)
|
|
assert not tool.is_leap_year(1800)
|
|
assert not tool.is_leap_year(1900)
|
|
assert not tool.is_leap_year(2100)
|
|
|
|
# ---- Julian leap year tests ----
|
|
def test_julian_leap_years(self):
|
|
"""Test Julian calendar leap years."""
|
|
tool = LeapYearTool(calendar="julian")
|
|
|
|
# Every 4th year is a leap year in Julian calendar
|
|
leap_years = [1896, 1900, 1904, 1908, 2000, 2004, 2008]
|
|
for year in leap_years:
|
|
assert tool.is_leap_year(year), f"{year} should be a leap year in Julian calendar"
|
|
|
|
# Non-leap years
|
|
non_leap_years = [1897, 1898, 1899, 1901, 1902, 1903, 2001, 2002, 2003]
|
|
for year in non_leap_years:
|
|
assert not tool.is_leap_year(year), f"{year} should not be a leap year in Julian calendar"
|
|
|
|
# ---- Algorithm correctness tests ----
|
|
def test_ben_joffe_algorithm_gregorian(self):
|
|
"""Test Ben Joffe's algorithm for Gregorian calendar."""
|
|
tool = LeapYearTool(calendar="gregorian")
|
|
|
|
# Test the algorithm directly
|
|
test_years = [1900, 2000, 2024, 2100, 2023, 2025]
|
|
|
|
for year in test_years:
|
|
# Ben Joffe's algorithm: (year & 3) == 0 and ((year % 25) != 0 or (year & 15) == 0)
|
|
expected = (year & 3) == 0 and ((year % 25) != 0 or (year & 15) == 0)
|
|
actual = tool.is_leap_year(year)
|
|
assert actual == expected, f"Algorithm mismatch for {year}"
|
|
|
|
def test_ben_joffe_algorithm_julian(self):
|
|
"""Test Ben Joffe's algorithm for Julian calendar."""
|
|
tool = LeapYearTool(calendar="julian")
|
|
|
|
# Test the algorithm directly: (year & 3) == 0
|
|
test_years = [1896, 1897, 1898, 1899, 1900, 1904, 2000, 2001]
|
|
|
|
for year in test_years:
|
|
expected = (year & 3) == 0
|
|
actual = tool.is_leap_year(year)
|
|
assert actual == expected, f"Algorithm mismatch for {year} in Julian calendar"
|
|
|
|
# ---- Batch operations tests ----
|
|
def test_find_leap_years(self):
|
|
"""Test finding leap years in a range."""
|
|
tool = LeapYearTool(calendar="gregorian")
|
|
|
|
leap_years = tool.find_leap_years(2000, 2020)
|
|
expected = [2000, 2004, 2008, 2012, 2016, 2020]
|
|
assert leap_years == expected
|
|
|
|
def test_count_leap_years(self):
|
|
"""Test counting leap years in a range."""
|
|
tool = LeapYearTool(calendar="gregorian")
|
|
|
|
count = tool.count_leap_years(2000, 2020)
|
|
assert count == 6 # 2000, 2004, 2008, 2012, 2016, 2020
|
|
|
|
def test_is_leap_year_batch(self):
|
|
"""Test batch leap year checking."""
|
|
tool = LeapYearTool(calendar="gregorian")
|
|
|
|
years = [2000, 2001, 2004, 2005, 2100, 2104]
|
|
results = tool.is_leap_year_batch(years)
|
|
expected = [True, False, True, False, False, True]
|
|
assert results == expected
|
|
|
|
def test_leap_year_range_iterator(self):
|
|
"""Test leap year range iterator."""
|
|
tool = LeapYearTool(calendar="gregorian")
|
|
|
|
leap_years = list(tool.leap_year_range(2000, 2020))
|
|
expected = [2000, 2004, 2008, 2012, 2016, 2020]
|
|
assert leap_years == expected
|
|
|
|
# ---- Date validation tests ----
|
|
def test_is_valid_date(self):
|
|
"""Test date validation."""
|
|
tool = LeapYearTool()
|
|
|
|
# Valid dates
|
|
assert tool.is_valid_date(2024, 1, 31) # January
|
|
assert tool.is_valid_date(2024, 2, 29) # Leap year February
|
|
assert tool.is_valid_date(2023, 2, 28) # Non-leap year February
|
|
assert tool.is_valid_date(2024, 12, 31) # December
|
|
|
|
# Invalid dates
|
|
assert not tool.is_valid_date(2023, 2, 29) # Non-leap year February 29
|
|
assert not tool.is_valid_date(2024, 2, 30) # Invalid February day
|
|
assert not tool.is_valid_date(2024, 4, 31) # April has 30 days
|
|
assert not tool.is_valid_date(2024, 13, 1) # Invalid month
|
|
assert not tool.is_valid_date(2024, 1, 0) # Invalid day
|
|
|
|
def test_get_days_in_month(self):
|
|
"""Test days in month calculation."""
|
|
tool = LeapYearTool()
|
|
|
|
# Regular months
|
|
assert tool.get_days_in_month(2024, 1) == 31
|
|
assert tool.get_days_in_month(2024, 4) == 30
|
|
assert tool.get_days_in_month(2024, 6) == 30
|
|
|
|
# February in leap year
|
|
assert tool.get_days_in_month(2024, 2) == 29
|
|
|
|
# February in non-leap year
|
|
assert tool.get_days_in_month(2023, 2) == 28
|
|
|
|
def test_get_days_in_year(self):
|
|
"""Test days in year calculation."""
|
|
tool = LeapYearTool()
|
|
|
|
assert tool.get_days_in_year(2024) == 366 # Leap year
|
|
assert tool.get_days_in_year(2023) == 365 # Non-leap year
|
|
|
|
# ---- Calendar utilities tests ----
|
|
def test_get_calendar_info(self):
|
|
"""Test calendar information retrieval."""
|
|
tool = LeapYearTool()
|
|
|
|
info = tool.get_calendar_info(2024)
|
|
|
|
assert info["year"] == 2024
|
|
assert info["calendar"] == "gregorian"
|
|
assert info["is_leap_year"] is True
|
|
assert info["days_in_year"] == 366
|
|
assert info["days_in_february"] == 29
|
|
assert len(info["monthly_days"]) == 12
|
|
assert info["monthly_days"][1] == 29 # February
|
|
|
|
def test_get_easter_date(self):
|
|
"""Test Easter date calculation."""
|
|
tool = LeapYearTool(calendar="gregorian")
|
|
|
|
# Known Easter dates
|
|
easter_2024 = tool.get_easter_date(2024)
|
|
assert easter_2024 == (3, 31) # March 31, 2024
|
|
|
|
easter_2025 = tool.get_easter_date(2025)
|
|
assert easter_2025 == (4, 20) # April 20, 2025
|
|
|
|
# ---- Performance and caching tests ----
|
|
def test_caching_enabled(self):
|
|
"""Test that caching works correctly."""
|
|
tool = LeapYearTool(enable_cache=True, cache_size=100)
|
|
|
|
# First call (cache miss)
|
|
result1 = tool.is_leap_year(2024)
|
|
stats = tool.get_cache_stats()
|
|
assert stats["hits"] == 0
|
|
assert stats["misses"] == 1
|
|
|
|
# Second call (cache hit)
|
|
result2 = tool.is_leap_year(2024)
|
|
stats = tool.get_cache_stats()
|
|
assert stats["hits"] == 1
|
|
assert stats["misses"] == 1
|
|
assert result1 == result2
|
|
|
|
def test_caching_disabled(self):
|
|
"""Test that caching can be disabled."""
|
|
tool = LeapYearTool(enable_cache=False)
|
|
|
|
tool.is_leap_year(2024)
|
|
stats = tool.get_cache_stats()
|
|
assert stats["enabled"] is False
|
|
|
|
def test_reset_cache_stats(self):
|
|
"""Test cache statistics reset."""
|
|
tool = LeapYearTool()
|
|
|
|
tool.is_leap_year(2024)
|
|
tool.reset_cache_stats()
|
|
|
|
stats = tool.get_cache_stats()
|
|
assert stats["hits"] == 0
|
|
assert stats["misses"] == 0
|
|
|
|
def test_benchmark_algorithm(self):
|
|
"""Test algorithm benchmarking."""
|
|
tool = LeapYearTool()
|
|
|
|
years = list(range(1900, 2100))
|
|
stats = tool.benchmark_algorithm(years, iterations=10)
|
|
|
|
assert "total_time" in stats
|
|
assert "iterations" in stats
|
|
assert "years_tested" in stats
|
|
assert "total_calculations" in stats
|
|
assert "average_time_per_calculation" in stats
|
|
assert "calculations_per_second" in stats
|
|
|
|
assert stats["iterations"] == 10
|
|
assert stats["years_tested"] == 200
|
|
assert stats["total_calculations"] == 2000
|
|
|
|
# ---- Configuration tests ----
|
|
def test_set_calendar(self):
|
|
"""Test calendar system switching."""
|
|
tool = LeapYearTool()
|
|
|
|
# Switch to Julian
|
|
tool.set_calendar("julian")
|
|
assert tool.calendar == "julian"
|
|
|
|
# Switch back to Gregorian
|
|
tool.set_calendar("gregorian")
|
|
assert tool.calendar == "gregorian"
|
|
|
|
# Invalid calendar
|
|
with pytest.raises(ValueError):
|
|
tool.set_calendar("invalid")
|
|
|
|
def test_enable_disable_caching(self):
|
|
"""Test caching enable/disable."""
|
|
tool = LeapYearTool(enable_cache=False)
|
|
|
|
# Enable caching
|
|
tool.enable_caching(cache_size=5000)
|
|
assert tool.enable_cache is True
|
|
assert tool.cache_size == 5000
|
|
|
|
# Disable caching
|
|
tool.disable_caching()
|
|
assert tool.enable_cache is False
|
|
|
|
# ---- Convenience methods tests ----
|
|
def test_next_leap_year(self):
|
|
"""Test finding next leap year."""
|
|
tool = LeapYearTool()
|
|
|
|
assert tool.next_leap_year(2023) == 2024
|
|
assert tool.next_leap_year(2024) == 2028
|
|
assert tool.next_leap_year(2099) == 2104
|
|
|
|
def test_previous_leap_year(self):
|
|
"""Test finding previous leap year."""
|
|
tool = LeapYearTool()
|
|
|
|
assert tool.previous_leap_year(2025) == 2024
|
|
assert tool.previous_leap_year(2024) == 2020
|
|
assert tool.previous_leap_year(1901) == 1896
|
|
|
|
def test_previous_leap_year_error(self):
|
|
"""Test error when no previous leap year exists."""
|
|
tool = LeapYearTool(min_year=2000)
|
|
|
|
with pytest.raises(ValueError, match="No previous leap year"):
|
|
tool.previous_leap_year(2000)
|
|
|
|
def test_get_leap_year_cycle(self):
|
|
"""Test leap year cycle calculation."""
|
|
tool = LeapYearTool()
|
|
|
|
# Year in leap year position
|
|
cycle = tool.get_leap_year_cycle(2024)
|
|
assert cycle == (2021, 2022, 2023, 2024)
|
|
|
|
# Year in non-leap position
|
|
cycle = tool.get_leap_year_cycle(2025)
|
|
assert cycle == (2021, 2022, 2023, 2024)
|
|
|
|
def test_direct_calendar_methods(self):
|
|
"""Test direct calendar method calls."""
|
|
# Gregorian
|
|
assert LeapYearTool().is_gregorian_leap_year(2000) is True
|
|
assert LeapYearTool().is_gregorian_leap_year(1900) is False
|
|
|
|
# Julian
|
|
assert LeapYearTool().is_julian_leap_year(1900) is True
|
|
assert LeapYearTool().is_julian_leap_year(1899) is False
|
|
|
|
# ---- Thread safety tests ----
|
|
def test_thread_safety(self):
|
|
"""Test that the tool is thread-safe."""
|
|
tool = LeapYearTool()
|
|
|
|
results = []
|
|
errors = []
|
|
|
|
def worker():
|
|
"""Worker function for thread safety test."""
|
|
try:
|
|
for _ in range(100):
|
|
result = tool.is_leap_year(2024)
|
|
results.append(result)
|
|
except Exception as e:
|
|
errors.append(e)
|
|
|
|
threads = []
|
|
for _ in range(10):
|
|
t = threading.Thread(target=worker)
|
|
threads.append(t)
|
|
t.start()
|
|
|
|
for t in threads:
|
|
t.join()
|
|
|
|
assert len(errors) == 0, f"Errors occurred: {errors}"
|
|
assert all(results), "All results should be True"
|
|
assert len(results) == 1000 # 10 threads * 100 iterations
|
|
|
|
# ---- Edge cases tests ----
|
|
def test_boundary_years(self):
|
|
"""Test boundary year values."""
|
|
tool = LeapYearTool(min_year=1, max_year=9999)
|
|
|
|
# Test minimum year
|
|
assert tool.is_leap_year(1) is False # Not divisible by 4
|
|
|
|
# Test maximum year
|
|
assert tool.is_leap_year(9999) is False # Not divisible by 4
|
|
|
|
# Test year 4 (first leap year)
|
|
assert tool.is_leap_year(4) is True
|
|
|
|
def test_large_year_ranges(self):
|
|
"""Test with large year ranges."""
|
|
tool = LeapYearTool()
|
|
|
|
# Test a large range
|
|
start, end = 1000, 3000
|
|
count = tool.count_leap_years(start, end)
|
|
|
|
# Should be approximately 1/4 of the years, minus century exceptions
|
|
expected_approx = (end - start + 1) // 4
|
|
assert abs(count - expected_approx) < 10 # Allow some variation
|
|
|
|
def test_tool_lifecycle(self):
|
|
"""Test tool initialization and shutdown."""
|
|
tool = LeapYearTool()
|
|
|
|
# Test initialization
|
|
tool.initialize()
|
|
|
|
# Test shutdown
|
|
tool.shutdown()
|
|
|
|
def test_invalid_date_components(self):
|
|
"""Test invalid date component validation."""
|
|
tool = LeapYearTool()
|
|
|
|
# Test invalid month
|
|
with pytest.raises(ValueError, match="Month.*out of range"):
|
|
tool._validate_date_components(2024, 13, 1)
|
|
|
|
# Test invalid day
|
|
with pytest.raises(ValueError, match="Day.*out of range"):
|
|
tool._validate_date_components(2024, 2, 30)
|
|
|
|
# Test invalid types
|
|
with pytest.raises(TypeError, match="Month must be an integer"):
|
|
tool._validate_date_components(2024, "2", 1)
|
|
|
|
with pytest.raises(TypeError, match="Day must be an integer"):
|
|
tool._validate_date_components(2024, 2, "1")
|