feat: initial commit
This commit is contained in:
@@ -0,0 +1,653 @@
|
||||
"""
|
||||
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"
|
||||
Reference in New Issue
Block a user