""" Unit tests for app.services.excel_parser.parse_excel Covers all 7 sample Excel order files: TRB, Kugellager, CRB, Gleitlager, SRB_TORB, Linear_schiene, Anschlagplatten Each category class verifies: - Correct category_key detected - Correct template_name resolved - Expected number of data rows (non-empty rows) - Row indices (first data row is Excel row 4) - medias_rendering values parsed correctly - First row standard fields match expected values - Component count (both per-row and total) - Component fields (part_name lowercased, material, component_type) - No unexpected warnings - parsed_excel_to_dict / parsed_row_to_dict serialisation is correct The cross-file suite (TestAllFilesStructural) re-runs key invariants against every file to catch regressions quickly. The TestParseExcelErrors suite tests ValueError / warning paths without touching the real Excel files. """ from __future__ import annotations import sys from pathlib import Path import pytest # Ensure backend package is importable when running from any directory. BACKEND_DIR = Path(__file__).resolve().parent.parent if str(BACKEND_DIR) not in sys.path: sys.path.insert(0, str(BACKEND_DIR)) from app.services.excel_parser import ( ParsedExcel, ParsedRow, ParsedComponent, parse_excel, parsed_excel_to_dict, parsed_row_to_dict, _normalize_filename, _to_bool, ) # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- def _all_components(parsed: ParsedExcel) -> list[ParsedComponent]: """Flatten all components across all rows of a ParsedExcel.""" return [c for row in parsed.rows for c in row.components] # --------------------------------------------------------------------------- # TRB — Tapered Roller Bearings # --------------------------------------------------------------------------- class TestTRBParser: """All assertions derived from TRB_Testscope_20260128.xlsx.""" def test_category_detected(self, parsed_trb: ParsedExcel): assert parsed_trb.category_key == "TRB" def test_template_name(self, parsed_trb: ParsedExcel): assert parsed_trb.template_name == "Tapered Roller Bearings (TRB)" def test_row_count(self, parsed_trb: ParsedExcel): assert len(parsed_trb.rows) == 4 def test_row_indices(self, parsed_trb: ParsedExcel): assert [r.row_index for r in parsed_trb.rows] == [4, 5, 6, 7] def test_no_warnings(self, parsed_trb: ParsedExcel): assert parsed_trb.warnings == [] def test_first_row_ebene1(self, parsed_trb: ParsedExcel): assert parsed_trb.rows[0].ebene1 == "Wälz- und Gleitlager" def test_first_row_baureihe(self, parsed_trb: ParsedExcel): assert parsed_trb.rows[0].baureihe == "Kegelrollenlager" def test_first_row_pim_id(self, parsed_trb: ParsedExcel): assert parsed_trb.rows[0].pim_id == "2305091021" def test_first_row_gewaehltes_produkt(self, parsed_trb: ParsedExcel): assert parsed_trb.rows[0].gewaehltes_produkt == "F-802070.TR4-AM" def test_all_medias_rendering_true(self, parsed_trb: ParsedExcel): assert all(r.medias_rendering is True for r in parsed_trb.rows) def test_first_row_component_count(self, parsed_trb: ParsedExcel): assert len(parsed_trb.rows[0].components) == 20 def test_total_component_count(self, parsed_trb: ParsedExcel): assert sum(len(r.components) for r in parsed_trb.rows) == 31 def test_first_component_material(self, parsed_trb: ParsedExcel): assert parsed_trb.rows[0].components[0].material == "Stahl v2" def test_part_names_lowercase(self, parsed_trb: ParsedExcel): for comp in _all_components(parsed_trb): if comp.part_name: assert comp.part_name == comp.part_name.lower() def test_component_column_indices_gte_11(self, parsed_trb: ParsedExcel): for comp in _all_components(parsed_trb): assert comp.column_index >= 11 def test_serialisation_keys(self, parsed_trb: ParsedExcel): d = parsed_excel_to_dict(parsed_trb) assert d["category_key"] == "TRB" assert d["row_count"] == 4 assert len(d["rows"]) == 4 def test_serialised_row_has_components_list(self, parsed_trb: ParsedExcel): d = parsed_excel_to_dict(parsed_trb) assert isinstance(d["rows"][0]["components"], list) # --------------------------------------------------------------------------- # Kugellager — Ball Bearings # --------------------------------------------------------------------------- class TestKugellagerParser: """All assertions derived from Kugellager_Testscope_20260128.xlsx.""" def test_category_detected(self, parsed_kugellager: ParsedExcel): assert parsed_kugellager.category_key == "Kugellager" def test_template_name(self, parsed_kugellager: ParsedExcel): assert parsed_kugellager.template_name == "Kugellager (Ball Bearings)" def test_row_count(self, parsed_kugellager: ParsedExcel): assert len(parsed_kugellager.rows) == 9 def test_row_indices(self, parsed_kugellager: ParsedExcel): assert parsed_kugellager.rows[0].row_index == 4 assert parsed_kugellager.rows[-1].row_index == 12 def test_no_warnings(self, parsed_kugellager: ParsedExcel): assert parsed_kugellager.warnings == [] def test_first_row_ebene1(self, parsed_kugellager: ParsedExcel): assert parsed_kugellager.rows[0].ebene1 == "Wälz- und Gleitlager" def test_first_row_baureihe(self, parsed_kugellager: ParsedExcel): assert parsed_kugellager.rows[0].baureihe == "Axial-Rillenkugellager" def test_first_row_pim_id(self, parsed_kugellager: ParsedExcel): assert parsed_kugellager.rows[0].pim_id == "2305100101" def test_first_row_gewaehltes_produkt(self, parsed_kugellager: ParsedExcel): assert parsed_kugellager.rows[0].gewaehltes_produkt == "51413-MP" def test_all_medias_rendering_true(self, parsed_kugellager: ParsedExcel): assert all(r.medias_rendering is True for r in parsed_kugellager.rows) def test_total_component_count(self, parsed_kugellager: ParsedExcel): assert sum(len(r.components) for r in parsed_kugellager.rows) == 55 def test_first_component_material(self, parsed_kugellager: ParsedExcel): assert parsed_kugellager.rows[0].components[0].material == "Stahl v2" def test_part_names_lowercase(self, parsed_kugellager: ParsedExcel): for comp in _all_components(parsed_kugellager): if comp.part_name: assert comp.part_name == comp.part_name.lower() def test_serialisation_row_count(self, parsed_kugellager: ParsedExcel): assert parsed_excel_to_dict(parsed_kugellager)["row_count"] == 9 # --------------------------------------------------------------------------- # CRB — Cylindrical Roller Bearings # --------------------------------------------------------------------------- class TestCRBParser: """All assertions derived from CRB_Testscope_20260128.xlsx.""" def test_category_detected(self, parsed_crb: ParsedExcel): assert parsed_crb.category_key == "CRB" def test_template_name(self, parsed_crb: ParsedExcel): assert parsed_crb.template_name == "Cylindrical Roller Bearings (CRB)" def test_row_count(self, parsed_crb: ParsedExcel): assert len(parsed_crb.rows) == 4 def test_row_indices(self, parsed_crb: ParsedExcel): assert [r.row_index for r in parsed_crb.rows] == [4, 5, 6, 7] def test_no_warnings(self, parsed_crb: ParsedExcel): assert parsed_crb.warnings == [] def test_first_row_baureihe(self, parsed_crb: ParsedExcel): assert parsed_crb.rows[0].baureihe == "Axial-Zylinderrollenlager" def test_first_row_pim_id(self, parsed_crb: ParsedExcel): assert parsed_crb.rows[0].pim_id == "2305110102" def test_first_row_gewaehltes_produkt(self, parsed_crb: ParsedExcel): assert parsed_crb.rows[0].gewaehltes_produkt == "893..-M" def test_all_medias_rendering_true(self, parsed_crb: ParsedExcel): assert all(r.medias_rendering is True for r in parsed_crb.rows) def test_first_row_component_count(self, parsed_crb: ParsedExcel): assert len(parsed_crb.rows[0].components) == 4 def test_total_component_count(self, parsed_crb: ParsedExcel): assert sum(len(r.components) for r in parsed_crb.rows) == 13 def test_first_component_material(self, parsed_crb: ParsedExcel): assert parsed_crb.rows[0].components[0].material == "Stahl v2" def test_cad_model_names_lowercase(self, parsed_crb: ParsedExcel): for row in parsed_crb.rows: if row.name_cad_modell: assert row.name_cad_modell == row.name_cad_modell.lower() def test_serialisation(self, parsed_crb: ParsedExcel): d = parsed_excel_to_dict(parsed_crb) assert d["category_key"] == "CRB" assert d["row_count"] == 4 # --------------------------------------------------------------------------- # Gleitlager — Plain Bearings # --------------------------------------------------------------------------- class TestGleitlagerParser: """All assertions derived from Gleitlager_Testscope_20260128.xlsx.""" def test_category_detected(self, parsed_gleitlager: ParsedExcel): assert parsed_gleitlager.category_key == "Gleitlager" def test_template_name(self, parsed_gleitlager: ParsedExcel): assert parsed_gleitlager.template_name == "Gleitlager (Plain Bearings)" def test_row_count(self, parsed_gleitlager: ParsedExcel): assert len(parsed_gleitlager.rows) == 3 def test_row_indices(self, parsed_gleitlager: ParsedExcel): assert [r.row_index for r in parsed_gleitlager.rows] == [4, 5, 6] def test_no_warnings(self, parsed_gleitlager: ParsedExcel): assert parsed_gleitlager.warnings == [] def test_first_row_baureihe(self, parsed_gleitlager: ParsedExcel): assert parsed_gleitlager.rows[0].baureihe == "Gelenklager" def test_first_row_pim_id_is_none(self, parsed_gleitlager: ParsedExcel): # Gleitlager first row has no PIM-ID assert parsed_gleitlager.rows[0].pim_id is None def test_first_row_gewaehltes_produkt(self, parsed_gleitlager: ParsedExcel): assert parsed_gleitlager.rows[0].gewaehltes_produkt == "GE..-HF" def test_all_medias_rendering_true(self, parsed_gleitlager: ParsedExcel): assert all(r.medias_rendering is True for r in parsed_gleitlager.rows) def test_total_component_count(self, parsed_gleitlager: ParsedExcel): assert sum(len(r.components) for r in parsed_gleitlager.rows) == 6 def test_first_component_material(self, parsed_gleitlager: ParsedExcel): assert parsed_gleitlager.rows[0].components[0].material == "Durotect CMT" def test_serialisation(self, parsed_gleitlager: ParsedExcel): d = parsed_excel_to_dict(parsed_gleitlager) assert d["category_key"] == "Gleitlager" assert d["row_count"] == 3 # --------------------------------------------------------------------------- # SRB_TORB — Spherical / Toroidal Roller Bearings # --------------------------------------------------------------------------- class TestSRBTORBParser: """All assertions derived from SRB_TORB_Testscope_20260128.xlsx.""" def test_category_detected(self, parsed_srb_torb: ParsedExcel): assert parsed_srb_torb.category_key == "SRB_TORB" def test_template_name(self, parsed_srb_torb: ParsedExcel): assert parsed_srb_torb.template_name == "Spherical / Toroidal Roller Bearings (SRB/TORB)" def test_row_count(self, parsed_srb_torb: ParsedExcel): assert len(parsed_srb_torb.rows) == 2 def test_row_indices(self, parsed_srb_torb: ParsedExcel): assert [r.row_index for r in parsed_srb_torb.rows] == [4, 5] def test_no_warnings(self, parsed_srb_torb: ParsedExcel): assert parsed_srb_torb.warnings == [] def test_first_row_baureihe(self, parsed_srb_torb: ParsedExcel): assert parsed_srb_torb.rows[0].baureihe == "Radial SRB" def test_first_row_pim_id(self, parsed_srb_torb: ParsedExcel): assert parsed_srb_torb.rows[0].pim_id == "2305091102" def test_first_row_gewaehltes_produkt(self, parsed_srb_torb: ParsedExcel): assert parsed_srb_torb.rows[0].gewaehltes_produkt == "241..-BE-XL-K30-H40" def test_all_medias_rendering_true(self, parsed_srb_torb: ParsedExcel): assert all(r.medias_rendering is True for r in parsed_srb_torb.rows) def test_first_row_component_count(self, parsed_srb_torb: ParsedExcel): assert len(parsed_srb_torb.rows[0].components) == 4 def test_total_component_count(self, parsed_srb_torb: ParsedExcel): assert sum(len(r.components) for r in parsed_srb_torb.rows) == 8 def test_first_component_material(self, parsed_srb_torb: ParsedExcel): assert parsed_srb_torb.rows[0].components[0].material == "Stahl v2" def test_serialisation(self, parsed_srb_torb: ParsedExcel): d = parsed_excel_to_dict(parsed_srb_torb) assert d["category_key"] == "SRB_TORB" assert d["row_count"] == 2 # --------------------------------------------------------------------------- # Linear_schiene — Linear Guide Rails # --------------------------------------------------------------------------- class TestLinearSchieneParser: """All assertions derived from Linear_schiene_Testscope_20260128.xlsx.""" def test_category_detected(self, parsed_linear_schiene: ParsedExcel): assert parsed_linear_schiene.category_key == "Linear_schiene" def test_template_name(self, parsed_linear_schiene: ParsedExcel): assert parsed_linear_schiene.template_name == "Linear Guide Rails" def test_row_count(self, parsed_linear_schiene: ParsedExcel): assert len(parsed_linear_schiene.rows) == 1 def test_row_index_starts_at_4(self, parsed_linear_schiene: ParsedExcel): assert parsed_linear_schiene.rows[0].row_index == 4 def test_no_warnings(self, parsed_linear_schiene: ParsedExcel): assert parsed_linear_schiene.warnings == [] def test_first_row_ebene1(self, parsed_linear_schiene: ParsedExcel): assert parsed_linear_schiene.rows[0].ebene1 == "Linearsysteme" def test_first_row_baureihe(self, parsed_linear_schiene: ParsedExcel): assert parsed_linear_schiene.rows[0].baureihe == "Rollenumlaufeinheiten" def test_first_row_pim_id(self, parsed_linear_schiene: ParsedExcel): assert parsed_linear_schiene.rows[0].pim_id == "233092AB21" def test_first_row_gewaehltes_produkt(self, parsed_linear_schiene: ParsedExcel): assert parsed_linear_schiene.rows[0].gewaehltes_produkt == "TSX..-D" def test_medias_rendering(self, parsed_linear_schiene: ParsedExcel): assert parsed_linear_schiene.rows[0].medias_rendering is True def test_component_count(self, parsed_linear_schiene: ParsedExcel): assert len(parsed_linear_schiene.rows[0].components) == 1 def test_first_component_part_name(self, parsed_linear_schiene: ParsedExcel): comp = parsed_linear_schiene.rows[0].components[0] assert comp.part_name == "tsx25d-g1-hj-gen.prt" def test_first_component_material(self, parsed_linear_schiene: ParsedExcel): comp = parsed_linear_schiene.rows[0].components[0] assert comp.material == "Stahl v2" def test_serialisation(self, parsed_linear_schiene: ParsedExcel): d = parsed_excel_to_dict(parsed_linear_schiene) assert d["category_key"] == "Linear_schiene" assert d["row_count"] == 1 # --------------------------------------------------------------------------- # Anschlagplatten — End Plates # --------------------------------------------------------------------------- class TestAnschlagplattenParser: """All assertions derived from Anschlagplatten_Testscope_20260128.xlsx.""" def test_category_detected(self, parsed_anschlagplatten: ParsedExcel): assert parsed_anschlagplatten.category_key == "Anschlagplatten" def test_template_name(self, parsed_anschlagplatten: ParsedExcel): assert parsed_anschlagplatten.template_name == "End Plates (Anschlagplatten)" def test_row_count(self, parsed_anschlagplatten: ParsedExcel): assert len(parsed_anschlagplatten.rows) == 2 def test_row_indices(self, parsed_anschlagplatten: ParsedExcel): assert [r.row_index for r in parsed_anschlagplatten.rows] == [4, 5] def test_no_warnings(self, parsed_anschlagplatten: ParsedExcel): assert parsed_anschlagplatten.warnings == [] def test_first_row_ebene1(self, parsed_anschlagplatten: ParsedExcel): assert parsed_anschlagplatten.rows[0].ebene1 == "Linearsysteme" def test_first_row_baureihe(self, parsed_anschlagplatten: ParsedExcel): assert parsed_anschlagplatten.rows[0].baureihe == "Endplatten für Führungsschiene LFS" def test_first_row_pim_id(self, parsed_anschlagplatten: ParsedExcel): assert parsed_anschlagplatten.rows[0].pim_id == "233092AM41" def test_first_row_gewaehltes_produkt(self, parsed_anschlagplatten: ParsedExcel): assert parsed_anschlagplatten.rows[0].gewaehltes_produkt == "ANS.LFS52-FH" def test_all_medias_rendering_true(self, parsed_anschlagplatten: ParsedExcel): assert all(r.medias_rendering is True for r in parsed_anschlagplatten.rows) def test_total_component_count(self, parsed_anschlagplatten: ParsedExcel): assert sum(len(r.components) for r in parsed_anschlagplatten.rows) == 3 def test_first_component_part_name(self, parsed_anschlagplatten: ParsedExcel): comp = parsed_anschlagplatten.rows[0].components[0] assert comp.part_name == "ans_lfs52-fh-0011_p.prt" def test_first_component_material(self, parsed_anschlagplatten: ParsedExcel): comp = parsed_anschlagplatten.rows[0].components[0] assert comp.material == "Stahl brüniert" def test_serialisation(self, parsed_anschlagplatten: ParsedExcel): d = parsed_excel_to_dict(parsed_anschlagplatten) assert d["category_key"] == "Anschlagplatten" assert d["row_count"] == 2 # --------------------------------------------------------------------------- # Cross-file structural invariants # --------------------------------------------------------------------------- class TestAllFilesStructural: """Invariants that must hold for every one of the 7 sample files.""" ALL_CATEGORIES = [ "TRB", "Kugellager", "CRB", "Gleitlager", "SRB_TORB", "Linear_schiene", "Anschlagplatten", ] def test_all_categories_detected(self, parsed_excel_all: dict): for cat in self.ALL_CATEGORIES: assert parsed_excel_all[cat].category_key == cat def test_all_have_template_names(self, parsed_excel_all: dict): for cat, parsed in parsed_excel_all.items(): assert parsed.template_name is not None, f"{cat}: template_name is None" def test_all_have_at_least_one_row(self, parsed_excel_all: dict): for cat, parsed in parsed_excel_all.items(): assert len(parsed.rows) > 0, f"{cat}: no data rows parsed" def test_all_rows_start_at_index_4(self, parsed_excel_all: dict): for cat, parsed in parsed_excel_all.items(): assert parsed.rows[0].row_index == 4, ( f"{cat}: first row_index is {parsed.rows[0].row_index}, expected 4" ) def test_row_indices_monotonically_increasing(self, parsed_excel_all: dict): for cat, parsed in parsed_excel_all.items(): indices = [r.row_index for r in parsed.rows] assert indices == sorted(indices), f"{cat}: row indices not ascending: {indices}" def test_all_medias_rendering_true(self, parsed_excel_all: dict): for cat, parsed in parsed_excel_all.items(): for row in parsed.rows: assert row.medias_rendering is True, ( f"{cat} row {row.row_index}: medias_rendering={row.medias_rendering}" ) def test_all_files_have_no_warnings(self, parsed_excel_all: dict): for cat, parsed in parsed_excel_all.items(): assert parsed.warnings == [], f"{cat} produced warnings: {parsed.warnings}" def test_all_component_column_indices_gte_11(self, parsed_excel_all: dict): for cat, parsed in parsed_excel_all.items(): for row in parsed.rows: for comp in row.components: assert comp.column_index >= 11, ( f"{cat} row {row.row_index}: component column_index={comp.column_index}" ) def test_all_part_names_lowercase(self, parsed_excel_all: dict): """The parser normalises filenames to lowercase.""" for cat, parsed in parsed_excel_all.items(): for row in parsed.rows: for comp in row.components: if comp.part_name: assert comp.part_name == comp.part_name.lower(), ( f"{cat} row {row.row_index}: part_name not lowercase: {comp.part_name!r}" ) def test_all_cad_model_names_lowercase(self, parsed_excel_all: dict): for cat, parsed in parsed_excel_all.items(): for row in parsed.rows: if row.name_cad_modell: assert row.name_cad_modell == row.name_cad_modell.lower(), ( f"{cat} row {row.row_index}: name_cad_modell not lowercase: {row.name_cad_modell!r}" ) def test_all_have_at_least_11_column_headers(self, parsed_excel_all: dict): for cat, parsed in parsed_excel_all.items(): assert len(parsed.column_headers) >= 11, ( f"{cat}: only {len(parsed.column_headers)} column headers (expected >= 11)" ) def test_serialised_dict_required_keys(self, parsed_excel_all: dict): required = { "filename", "category_key", "template_name", "row_count", "column_headers", "rows", "warnings", } for cat, parsed in parsed_excel_all.items(): d = parsed_excel_to_dict(parsed) missing = required - d.keys() assert not missing, f"{cat}: serialised dict missing keys: {missing}" def test_serialised_row_required_keys(self, parsed_excel_all: dict): required = { "row_index", "ebene1", "ebene2", "baureihe", "pim_id", "produkt_baureihe", "gewaehltes_produkt", "name_cad_modell", "gewuenschte_bildnummer", "lagertyp", "medias_rendering", "components", } for cat, parsed in parsed_excel_all.items(): for row in parsed.rows: d = parsed_row_to_dict(row) missing = required - d.keys() assert not missing, ( f"{cat} row {row.row_index}: serialised row missing keys: {missing}" ) def test_serialised_component_required_keys(self, parsed_excel_all: dict): required = {"part_name", "material", "component_type", "column_index"} for cat, parsed in parsed_excel_all.items(): for row in parsed.rows: for comp_d in parsed_row_to_dict(row)["components"]: missing = required - comp_d.keys() assert not missing, ( f"{cat} row {row.row_index}: component dict missing keys: {missing}" ) def test_serialised_row_count_matches(self, parsed_excel_all: dict): for cat, parsed in parsed_excel_all.items(): d = parsed_excel_to_dict(parsed) assert d["row_count"] == len(d["rows"]) == len(parsed.rows) # --------------------------------------------------------------------------- # Internal helper unit tests # --------------------------------------------------------------------------- class TestNormalizeFilename: def test_lowercases_extension(self): assert _normalize_filename("TEST.PRT") == "test.prt" def test_strips_leading_trailing_spaces(self): assert _normalize_filename(" 81113-L_cut.stp ") == "81113-l_cut.stp" def test_none_returns_none(self): assert _normalize_filename(None) is None def test_empty_string_returns_none(self): # _normalize_filename("") returns "" which the _clean wrapper converts to None # In the parser _normalize_filename wraps _clean, so empty → None result = _normalize_filename("") # The function strips and lowercases; empty string stays empty (falsy) assert result == "" or result is None class TestToBool: @pytest.mark.parametrize("val,expected", [ (1, True), (0, False), (True, True), (False, False), ("1", True), ("0", False), ("ja", True), ("Ja", True), ("nein", False), ("Nein", False), ("yes", True), ("no", False), ("x", True), ("", False), (None, None), ]) def test_to_bool_parametrize(self, val, expected): assert _to_bool(val) == expected def test_medias_rendering_is_bool_or_none(self, parsed_excel_all: dict): for cat, parsed in parsed_excel_all.items(): for row in parsed.rows: assert row.medias_rendering in (True, False, None), ( f"{cat} row {row.row_index}: unexpected medias_rendering={row.medias_rendering!r}" ) # --------------------------------------------------------------------------- # Error handling # --------------------------------------------------------------------------- class TestParseExcelErrors: def test_nonexistent_file_raises(self, tmp_path: Path): with pytest.raises(ValueError, match="Cannot open Excel file"): parse_excel(tmp_path / "does_not_exist.xlsx") def test_too_few_rows_raises(self, tmp_path: Path): import openpyxl wb = openpyxl.Workbook() ws = wb.active ws.append(["only one row"]) ws.append(["only two rows"]) path = tmp_path / "short.xlsx" wb.save(path) with pytest.raises(ValueError, match="fewer than 3 rows"): parse_excel(path) def test_empty_data_rows_produces_warning(self, tmp_path: Path): """A file with valid headers but zero data rows should warn, not raise.""" import openpyxl wb = openpyxl.Workbook() ws = wb.active ws.append(["Instructions row 1"]) ws.append(["Instructions row 2"]) ws.append([ "Ebene1", "Ebene2", "Baureihe", "PIM", "Produkt", "SEP", "Produkt", "Name", "Bildnr", "Lagertyp", "Medias", ]) # Intentionally no data rows path = tmp_path / "no_data.xlsx" wb.save(path) result = parse_excel(path) assert result.rows == [] assert len(result.warnings) > 0 def test_parse_accepts_pathlib_path(self, excel_paths: dict): """parse_excel should accept a Path object, not just a string.""" path = excel_paths["TRB"] assert isinstance(path, Path) result = parse_excel(path) assert result.category_key == "TRB" def test_parse_accepts_string_path(self, excel_paths: dict): """parse_excel should also accept a plain string path.""" result = parse_excel(str(excel_paths["CRB"])) assert result.category_key == "CRB"