"""Tests des écarts d'inventaire calculés depuis parts_filtered.csv.""" import csv from pathlib import Path from lib.rebrickable.inventory_reconciliation import ( compute_inventory_gaps, index_sets_by_num, write_inventory_gaps_csv, write_inventory_gaps_markdown, ) def write_csv(path: Path, headers: list[str], rows: list[list[str]]) -> None: """Écrit un CSV simple pour les besoins des tests.""" with path.open("w", newline="") as csv_file: writer = csv.writer(csv_file) writer.writerow(headers) writer.writerows(rows) def test_compute_inventory_gaps_excludes_spares(tmp_path: Path) -> None: """Ignore les pièces de rechange et ne conserve que les sets en écart.""" sets_path = tmp_path / "sets_enriched.csv" parts_path = tmp_path / "parts_filtered.csv" write_csv( sets_path, ["set_num", "set_id", "num_parts", "in_collection"], [ ["1000-1", "1000", "4", "true"], ["2000-1", "2000", "3", "false"], ["3000-1", "3000", "1", "true"], ], ) write_csv( parts_path, ["part_num", "color_rgb", "is_translucent", "set_num", "set_id", "quantity_in_set", "is_spare"], [ ["A", "AAAAAA", "false", "1000-1", "1000", "2", "false"], ["B", "BBBBBB", "false", "1000-1", "1000", "2", "false"], ["S", "SSSSSS", "false", "1000-1", "1000", "5", "true"], ["C", "CCCCCC", "false", "2000-1", "2000", "2", "false"], ["D", "DDDDDD", "false", "3000-1", "3000", "1", "false"], ], ) gaps = compute_inventory_gaps(sets_path, parts_path) assert gaps == [ { "set_num": "1000-1", "set_id": "1000", "expected_parts": 4, "inventory_parts": 9, "inventory_parts_non_spare": 4, "delta": 5, "delta_non_spare": 0, "in_collection": "true", }, { "set_num": "2000-1", "set_id": "2000", "expected_parts": 3, "inventory_parts": 2, "inventory_parts_non_spare": 2, "delta": 1, "delta_non_spare": 1, "in_collection": "false", } ] def test_write_inventory_gaps_csv(tmp_path: Path) -> None: """Sérialise le rapport d'écarts dans un CSV dédié.""" destination_path = tmp_path / "inventory_gaps.csv" rows = [ { "set_num": "2000-1", "set_id": "2000", "expected_parts": 3, "inventory_parts": 2, "inventory_parts_non_spare": 2, "delta": 1, "delta_non_spare": 1, "in_collection": "false", } ] write_inventory_gaps_csv(destination_path, rows) with destination_path.open() as csv_file: written_rows = list(csv.DictReader(csv_file)) assert written_rows == [ { "set_num": "2000-1", "set_id": "2000", "expected_parts": "3", "inventory_parts": "2", "inventory_parts_non_spare": "2", "delta": "1", "delta_non_spare": "1", "in_collection": "false", } ] def test_write_inventory_gaps_markdown(tmp_path: Path) -> None: """Produit un tableau Markdown listant les sets en écart.""" destination_path = tmp_path / "inventory_gaps.md" gaps = [ { "set_num": "2000-1", "set_id": "2000", "expected_parts": 3, "inventory_parts": 2, "inventory_parts_non_spare": 2, "delta": 1, "delta_non_spare": 1, "in_collection": "false", } ] sets = [ { "set_num": "2000-1", "set_id": "2000", "num_parts": "3", "name": "Test Set", "year": "2020", "rebrickable_url": "https://rebrickable.com/sets/2000-1", "in_collection": "false", } ] write_inventory_gaps_markdown(destination_path, gaps, index_sets_by_num(sets)) with destination_path.open() as markdown_file: content = markdown_file.read().splitlines() assert content[0].startswith("| set_id | name |") assert ( "| [2000](https://rebrickable.com/sets/2000-1) | Test Set | 2020 | 1 | 1 | 3 | 2 | 2 | false | [PDF](https://www.lego.com/service/buildinginstructions/2000) |" in content )