""" TimeCalculator 단위 테스트. """ import os import sys from datetime import datetime, timedelta import pytest sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from core.time_calculator import TimeCalculator @pytest.fixture def calc_8h(): return TimeCalculator(work_hours=8, lunch_duration_minutes=60) @pytest.fixture def calc_short(): """단축근무 7h30m + 점심 30m""" return TimeCalculator(work_minutes=450, lunch_duration_minutes=30) @pytest.fixture def clock_in_9am(): return datetime(2026, 4, 29, 9, 0, 0) class TestClockOutTime: def test_standard_8h_with_lunch(self, calc_8h, clock_in_9am): co = calc_8h.calculate_clock_out_time(clock_in_9am, include_lunch=True) assert co == datetime(2026, 4, 29, 18, 0, 0) def test_short_7h30m_with_lunch(self, calc_short, clock_in_9am): co = calc_short.calculate_clock_out_time(clock_in_9am, include_lunch=True) assert co == datetime(2026, 4, 29, 17, 0, 0) def test_no_lunch(self, calc_8h, clock_in_9am): co = calc_8h.calculate_clock_out_time(clock_in_9am, include_lunch=False) assert co == datetime(2026, 4, 29, 17, 0, 0) def test_with_dinner(self, calc_8h, clock_in_9am): co = calc_8h.calculate_clock_out_time(clock_in_9am, include_lunch=True, include_dinner=True) assert co == datetime(2026, 4, 29, 19, 0, 0) def test_with_break_minutes(self, calc_8h, clock_in_9am): co_no = calc_8h.calculate_clock_out_time(clock_in_9am, include_lunch=True) co = calc_8h.calculate_clock_out_time(clock_in_9am, include_lunch=True, break_minutes=30) assert (co - co_no) == timedelta(minutes=30) @pytest.mark.parametrize("actual_min,expected_earned", [ (29, 0), # 30분 미만 절삭 (30, 30), (35, 30), (60, 60), (89, 60), (90, 90), (120, 120), ]) def test_overtime_30min_truncation(calc_8h, clock_in_9am, actual_min, expected_earned): base_co = clock_in_9am + timedelta(hours=8) actual_co = base_co + timedelta(minutes=actual_min) _, earned = calc_8h.calculate_overtime(clock_in_9am, actual_co, include_lunch=False) assert earned == expected_earned class TestCompatibility: def test_work_hours_property_returns_float(self): c = TimeCalculator(work_minutes=450) assert c.work_hours == 7.5 def test_work_hours_constructor_accepts_float(self): c = TimeCalculator(work_hours=7.5, lunch_duration_minutes=30) assert c.work_minutes == 450 def test_work_minutes_takes_precedence(self): # 둘 다 주면 work_minutes 우선 c = TimeCalculator(work_hours=8, work_minutes=450) assert c.work_minutes == 450 def test_default_8_hours(self): c = TimeCalculator() assert c.work_minutes == 480 class TestDayType: def test_weekend(self): calc = TimeCalculator() sat = datetime(2026, 5, 2) assert calc.is_weekend(sat) assert calc.get_day_type(sat) == 'weekend' def test_weekday(self): calc = TimeCalculator() mon = datetime(2026, 5, 4) assert not calc.is_weekend(mon) assert calc.get_day_type(mon) == 'normal'