Some checks failed
CI / test (push) Has been cancelled
핵심 기능: - 단축근무·표준·반일 등 다양한 근무 패턴 (5개 프리셋 + 사용자 정의) - Windows 이벤트 뷰어 자동 출퇴근 감지 - 30분 단위 연장근무 적립/사용 시스템 - 1.0/0.5/0.25일 연차·반차·반반차 - 자동 점심·저녁·외출·자동 백업·화면 잠금 자동 외출 - 한국 공휴일 자동 등록 (음력 포함, holidays 패키지) - matplotlib 차트 기반 주간/월간/패턴 통계 - 미니 위젯 + 시스템 트레이 통합 - 한국어/English i18n - 자가 업데이트 (updater.exe + Gitea Releases) 아키텍처: - core/ (db, time_calculator, notifier, i18n, version, settings_keys) - ui/ (main_window + 9 dialogs + 3 controllers) - utils/ (backup, lock_detector, debug_log, updater_client, time_format) - tests/ (66 pytest 단위) + 통합/i18n GUI 검증 CI/CD: - .gitea/workflows/ci.yml: push 시 pytest + 통합 테스트 - .gitea/workflows/release.yml: v* 태그 push 시 두 .exe 자동 빌드 + Releases 첨부 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
3.1 KiB
Python
101 lines
3.1 KiB
Python
"""
|
|
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'
|