erster versuch der KI mit Plan

This commit is contained in:
Rob committed 2026-03-27 13:18:07 +01:00
1 parent c697c64f3a
commit 2faa1689dd
20 files changed
+1749

No files matched your search

+278
View File
@@ -0,0 +1,278 @@
"""Tests for src/excel_reader.py — file-independent business logic."""
import re
from datetime import date
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from src.exceptions import ExcelDataError, ExcelSheetError, YearExtractionError
from src.excel_reader import (
_parse_cell_value,
extract_year_from_filename,
extract_persons,
extract_attendance,
)
from src.models import Person
# ---------------------------------------------------------------------------
# Minimal config fixture (no real file needed)
# ---------------------------------------------------------------------------
@pytest.fixture
def cfg():
return {
"excel_year_pattern": "(19\\d{2}|20\\d{2})",
"excel_name_header": "Name, Vorname",
"excel_name_column": 1,
"excel_seq_column": 0,
"excel_day_start_col": 9,
"excel_day_end_col": 39,
"month_mapping": {
"Jan": "01", "Feb": "02", "Mär": "03", "Mrz": "03",
"Apr": "04", "Mai": "05", "Jun": "06", "Jul": "07",
"Aug": "08", "Sep": "09", "Okt": "10", "Nov": "11", "Dez": "12",
},
"valid_status_codes": ["K", "KK", "E", "F", "P", "U"],
"plan_hours_value": "6",
"pauschale_value": "8,60",
"antragsnummer": "12345",
"ort": "Teststadt",
"template_pdf": "template/sab.pdf",
"output_dir": "output",
}
# ---------------------------------------------------------------------------
# extract_year_from_filename
# ---------------------------------------------------------------------------
class TestExtractYearFromFilename:
def test_standard_filename(self, cfg):
assert extract_year_from_filename("Anwesenheit_2026.xlsx", cfg) == 2026
def test_year_embedded_in_path(self, cfg):
assert extract_year_from_filename("/data/exports/Anwesenheit_2024.xlsx", cfg) == 2024
def test_no_year_raises(self, cfg):
with pytest.raises(YearExtractionError):
extract_year_from_filename("Anwesenheit.xlsx", cfg)
def test_out_of_range_raises(self, cfg):
# Overwrite pattern to force out-of-range year
bad_cfg = {**cfg, "excel_year_pattern": "(1800)"}
with pytest.raises(YearExtractionError):
extract_year_from_filename("data_1800.xlsx", bad_cfg)
def test_first_year_match_wins(self, cfg):
# Two years in name — first one is used
assert extract_year_from_filename("2023_to_2025_export.xlsx", cfg) == 2023
# ---------------------------------------------------------------------------
# _parse_cell_value
# ---------------------------------------------------------------------------
class TestParseCellValue:
VALID_CODES = ["K", "KK", "E", "F", "P", "U"]
def test_numeric_int(self):
result = _parse_cell_value(6, self.VALID_CODES)
assert result == ("X", 6.0)
def test_numeric_float(self):
result = _parse_cell_value(6.5, self.VALID_CODES)
assert result == ("X", 6.5)
def test_zero_skipped(self):
assert _parse_cell_value(0, self.VALID_CODES) is None
def test_negative_skipped(self):
assert _parse_cell_value(-1, self.VALID_CODES) is None
def test_none_skipped(self):
assert _parse_cell_value(None, self.VALID_CODES) is None
def test_empty_string_skipped(self):
assert _parse_cell_value("", self.VALID_CODES) is None
assert _parse_cell_value(" ", self.VALID_CODES) is None
def test_status_codes(self):
for code in self.VALID_CODES:
result = _parse_cell_value(code, self.VALID_CODES)
assert result is not None
assert result[0] == code
assert result[1] is None
def test_lowercase_status_code(self):
result = _parse_cell_value("k", self.VALID_CODES)
assert result is not None
assert result[0] == "K"
def test_explicit_x_no_hours(self):
result = _parse_cell_value("X", self.VALID_CODES)
assert result == ("X", None)
def test_unknown_string_skipped(self):
assert _parse_cell_value("UNBEKANNT", self.VALID_CODES) is None
# ---------------------------------------------------------------------------
# extract_persons (mocked workbook)
# ---------------------------------------------------------------------------
def _make_row(*values):
"""Create a list of values as a mock row."""
return list(values)
def _sheet_data_persons():
"""Simulated sheet data: header row + 3 persons."""
return [
[None, "Einrichtung XY", None], # row 0 – ignored
[None, "Name, Vorname", None], # row 1 – header
[1, "Mustermann, Max", None], # row 2 – person 1
[2, "Beispiel, Anna", None], # row 3 – person 2
[3, "Test, Peter", None], # row 4 – person 3
[None, None, None], # row 5 – empty row
]
class TestExtractPersons:
def test_returns_correct_persons(self, cfg):
with patch("src.excel_reader._open_workbook") as mock_open, \
patch("src.excel_reader._sheet_data", return_value=_sheet_data_persons()):
mock_wb = MagicMock()
mock_wb.sheetnames = ["Feb"]
mock_open.return_value = mock_wb
persons = extract_persons("dummy.xlsx", "Feb", cfg)
assert len(persons) == 3
assert persons[0].seq_num == 1
assert persons[1].name == "Beispiel, Anna"
assert persons[2].seq_num == 3
def test_empty_rows_ignored(self, cfg):
with patch("src.excel_reader._open_workbook") as mock_open, \
patch("src.excel_reader._sheet_data", return_value=_sheet_data_persons()):
mock_open.return_value = MagicMock(sheetnames=["Feb"])
persons = extract_persons("dummy.xlsx", "Feb", cfg)
assert all(p.name for p in persons)
# ---------------------------------------------------------------------------
# extract_attendance (mocked workbook)
# ---------------------------------------------------------------------------
def _sheet_data_attendance():
"""
Simulated month sheet with one person row.
Columns 0-8: seq, name, and spare cols.
Columns 9-39: days 1-31.
Day 1 (col 9): 6.0 → present 6h
Day 2 (col 10): "K" → sick
Day 3 (col 11): "KK" → child sick
Day 4 (col 12): None → skip
Day 5 (col 13): "F" → unexcused absence
"""
person_row = [None] * 40
person_row[0] = 1
person_row[1] = "Mustermann, Max"
person_row[9] = 6.0
person_row[10] = "K"
person_row[11] = "KK"
person_row[12] = None
person_row[13] = "F"
return [
[None, "Name, Vorname"] + [None] * 38, # header row
person_row,
]
class TestExtractAttendance:
def test_present_day(self, cfg):
with patch("src.excel_reader._open_workbook") as mock_open, \
patch("src.excel_reader._sheet_data", return_value=_sheet_data_attendance()):
mock_open.return_value = MagicMock(sheetnames=["Feb"])
person = Person(seq_num=1, name="Mustermann, Max")
attendance = extract_attendance("dummy.xlsx", "Feb", person, 2026, cfg)
entry = attendance.entries[0]
assert entry.status == "X"
assert entry.hours == 6.0
assert entry.date == date(2026, 2, 1)
def test_sick_day(self, cfg):
with patch("src.excel_reader._open_workbook") as mock_open, \
patch("src.excel_reader._sheet_data", return_value=_sheet_data_attendance()):
mock_open.return_value = MagicMock(sheetnames=["Feb"])
person = Person(seq_num=1, name="Mustermann, Max")
attendance = extract_attendance("dummy.xlsx", "Feb", person, 2026, cfg)
entry = attendance.entries[1]
assert entry.status == "K"
assert entry.hours is None
def test_empty_day_skipped(self, cfg):
with patch("src.excel_reader._open_workbook") as mock_open, \
patch("src.excel_reader._sheet_data", return_value=_sheet_data_attendance()):
mock_open.return_value = MagicMock(sheetnames=["Feb"])
person = Person(seq_num=1, name="Mustermann, Max")
attendance = extract_attendance("dummy.xlsx", "Feb", person, 2026, cfg)
# Day 4 (None) is skipped, so entry index 2 is Day 3 (KK) and index 3 is Day 5 (F)
assert len(attendance.entries) == 4
assert attendance.entries[2].status == "KK"
assert attendance.entries[3].status == "F"
def test_max_23_entries(self, cfg):
"""More than 23 valid entries must be truncated to 23."""
person_row = [None] * 40
person_row[0] = 1
person_row[1] = "Viel, Anwesend"
for col in range(9, 40): # 31 days all present
person_row[col] = 6.0
sheet_data = [
[None, "Name, Vorname"] + [None] * 38,
person_row,
]
with patch("src.excel_reader._open_workbook") as mock_open, \
patch("src.excel_reader._sheet_data", return_value=sheet_data):
mock_open.return_value = MagicMock(sheetnames=["Jan"])
person = Person(seq_num=1, name="Viel, Anwesend")
attendance = extract_attendance("dummy.xlsx", "Jan", person, 2026, cfg)
assert len(attendance.entries) == 23
def test_invalid_month_raises(self, cfg):
with pytest.raises(ExcelSheetError):
extract_attendance("dummy.xlsx", "UNGÜLTIG", Person(1, "X, Y"), 2026, cfg)
def test_february_day_29_invalid_year(self, cfg):
"""Day 29 in Feb of a non-leap year must be skipped."""
person_row = [None] * 40
person_row[0] = 1
person_row[1] = "Schaltjahr, Test"
person_row[9 + 27] = 6.0 # day 28 = col 36 → valid
person_row[9 + 28] = 6.0 # day 29 = col 37 → invalid (2025 not leap year)
person_row[9 + 29] = 6.0 # day 30 = col 38 → invalid
sheet_data = [
[None, "Name, Vorname"] + [None] * 38,
person_row,
]
with patch("src.excel_reader._open_workbook") as mock_open, \
patch("src.excel_reader._sheet_data", return_value=sheet_data):
mock_open.return_value = MagicMock(sheetnames=["Feb"])
person = Person(seq_num=1, name="Schaltjahr, Test")
attendance = extract_attendance("dummy.xlsx", "Feb", person, 2025, cfg)
# Only day 28 is valid in Feb 2025
assert len(attendance.entries) == 1
assert attendance.entries[0].date == date(2025, 2, 28)