"""Tests de l'agrégation des minifigs par personnage.""" from pathlib import Path from lib.rebrickable.minifig_characters import ( aggregate_by_character, aggregate_by_gender, aggregate_characters_by_gender, aggregate_new_character_sets, aggregate_new_characters_by_year, aggregate_variations_and_totals, aggregate_character_spans, aggregate_presence_by_year, load_sets_enriched, write_character_counts, write_character_gender_counts, write_new_character_sets_csv, write_new_character_sets_markdown, write_new_characters_by_year, write_character_variations_totals, write_gender_counts, ) from lib.rebrickable.minifig_character_sets import load_sets def test_aggregate_by_character_counts_unique_figs() -> None: """Compter les minifigs distinctes par personnage en excluant les noms vides.""" aggregates = aggregate_by_character( [ { "set_num": "123-1", "part_num": "head-a", "known_character": "Owen Grady", "fig_num": "fig-owen-1", "gender": "male", }, { "set_num": "124-1", "part_num": "head-b", "known_character": "Owen Grady", "fig_num": "fig-owen-1", "gender": "male", }, { "set_num": "125-1", "part_num": "head-c", "known_character": "Owen Grady", "fig_num": "fig-owen-2", "gender": "male", }, { "set_num": "126-1", "part_num": "head-d", "known_character": "Figurant", "fig_num": "fig-guard-1", "gender": "unknown", }, { "set_num": "128-1", "part_num": "head-f", "known_character": "Figurant", "fig_num": "fig-guard-1", "gender": "unknown", }, { "set_num": "129-1", "part_num": "head-g", "known_character": "", "fig_num": "fig-guard-2", "gender": "unknown", }, ] ) assert aggregates == [ {"known_character": "Owen Grady", "gender": "male", "minifig_count": 2}, {"known_character": "Figurant", "gender": "unknown", "minifig_count": 1}, ] def test_aggregate_variations_and_totals_excludes_figurants() -> None: """Compter le total et les variations en excluant les figurants.""" aggregates = aggregate_variations_and_totals( [ { "set_num": "123-1", "part_num": "head-a", "known_character": "Owen Grady", "fig_num": "fig-owen-1", "gender": "male", }, { "set_num": "124-1", "part_num": "head-b", "known_character": "Owen Grady", "fig_num": "fig-owen-1", "gender": "male", }, { "set_num": "125-1", "part_num": "head-c", "known_character": "Owen Grady", "fig_num": "fig-owen-2", "gender": "male", }, { "set_num": "126-1", "part_num": "head-d", "known_character": "Ellie Sattler", "fig_num": "fig-ellie-1", "gender": "female", }, { "set_num": "127-1", "part_num": "head-e", "known_character": "Figurant", "fig_num": "fig-guard-1", "gender": "unknown", }, ], excluded_characters=["Figurant"], ) assert aggregates == [ {"known_character": "Owen Grady", "gender": "male", "variation_count": 2, "total_minifigs": 3}, {"known_character": "Ellie Sattler", "gender": "female", "variation_count": 1, "total_minifigs": 1}, ] def test_aggregate_by_gender_counts_unique_figs() -> None: """Compter les minifigs distinctes par genre.""" aggregates = aggregate_by_gender( [ {"fig_num": "fig-a", "gender": "male"}, {"fig_num": "fig-a", "gender": "male"}, {"fig_num": "fig-b", "gender": "female"}, {"fig_num": "fig-c", "gender": ""}, ] ) assert aggregates == [ {"gender": "female", "minifig_count": "1"}, {"gender": "male", "minifig_count": "1"}, {"gender": "unknown", "minifig_count": "1"}, ] def test_aggregate_characters_by_gender_unique_characters() -> None: """Compter les personnages distincts par genre (ignorer unknown).""" aggregates = aggregate_characters_by_gender( [ {"known_character": "A", "gender": "male"}, {"known_character": "A", "gender": "male"}, {"known_character": "B", "gender": "female"}, {"known_character": "C", "gender": "unknown"}, ] ) assert aggregates == [ {"gender": "female", "character_count": "1"}, {"gender": "male", "character_count": "1"}, ] def test_aggregate_new_characters_by_year_limits_range(tmp_path: Path) -> None: """Compter les nouveaux personnages par année en respectant la plage.""" sets_path = tmp_path / "sets_enriched.csv" sets_path.write_text( "set_num,name,year,theme_id,num_parts,img_url,set_id,rebrickable_url,in_collection\n" "123-1,Set A,2015,0,0,,-,http://r/123-1,true\n" "124-1,Set B,2016,0,0,,-,http://r/124-1,true\n" "125-1,Set C,2017,0,0,,-,http://r/125-1,true\n" "126-1,Set D,2014,0,0,,-,http://r/126-1,true\n" ) sets_years = load_sets_enriched(sets_path) minifigs_rows = [ {"set_num": "123-1", "known_character": "Owen Grady", "fig_num": "fig-owen-1", "part_num": "head-a"}, {"set_num": "124-1", "known_character": "Owen Grady", "fig_num": "fig-owen-2", "part_num": "head-b"}, {"set_num": "125-1", "known_character": "Ellie Sattler", "fig_num": "fig-ellie-1", "part_num": "head-c"}, {"set_num": "126-1", "known_character": "Alan Grant", "fig_num": "fig-grant-1", "part_num": "head-d"}, ] counts = aggregate_new_characters_by_year( minifigs_rows, sets_years, excluded_characters=["Figurant"], start_year=2015, end_year=2017, ) assert counts == [ {"year": "2015", "new_characters": "1"}, {"year": "2016", "new_characters": "0"}, {"year": "2017", "new_characters": "1"}, ] def test_aggregate_new_character_sets_returns_intro_sets(tmp_path: Path) -> None: """Lister les personnages introduits avec les sets de l'année d'introduction.""" sets_path = tmp_path / "sets_enriched.csv" sets_path.write_text( "set_num,name,year,theme_id,num_parts,img_url,set_id,rebrickable_url,in_collection\n" "123-1,Set A,2015,0,0,,123,http://r/123-1,true\n" "124-1,Set B,2015,0,0,,124,http://r/124-1,true\n" "125-1,Set C,2016,0,0,,125,http://r/125-1,true\n" ) sets_lookup = load_sets(sets_path) minifigs_rows = [ {"set_num": "123-1", "known_character": "Owen Grady", "fig_num": "fig-owen-1", "part_num": "head-a"}, {"set_num": "124-1", "known_character": "Owen Grady", "fig_num": "fig-owen-2", "part_num": "head-b"}, {"set_num": "125-1", "known_character": "Owen Grady", "fig_num": "fig-owen-3", "part_num": "head-c"}, {"set_num": "125-1", "known_character": "Ellie Sattler", "fig_num": "fig-ellie-1", "part_num": "head-d"}, ] rows = aggregate_new_character_sets( minifigs_rows, sets_lookup, excluded_characters=["Figurant"], start_year=2015, end_year=2016, ) assert rows == [ { "year": "2015", "known_character": "Owen Grady", "set_num": "123-1", "set_id": "123", "set_name": "Set A", "rebrickable_url": "http://r/123-1", }, { "year": "2015", "known_character": "Owen Grady", "set_num": "124-1", "set_id": "124", "set_name": "Set B", "rebrickable_url": "http://r/124-1", }, { "year": "2016", "known_character": "Ellie Sattler", "set_num": "125-1", "set_id": "125", "set_name": "Set C", "rebrickable_url": "http://r/125-1", }, ] def test_write_character_counts_outputs_csv(tmp_path: Path) -> None: """Écrit le CSV des comptes par personnage.""" destination = tmp_path / "counts.csv" rows = [ {"known_character": "A", "gender": "male", "minifig_count": 2}, {"known_character": "B", "gender": "female", "minifig_count": 1}, ] write_character_counts(destination, rows) assert destination.read_text() == "known_character,gender,minifig_count\nA,male,2\nB,female,1\n" def test_write_gender_counts_outputs_csv(tmp_path: Path) -> None: """Écrit le CSV des comptes par genre.""" destination = tmp_path / "gender_counts.csv" rows = [ {"gender": "male", "minifig_count": "2"}, {"gender": "female", "minifig_count": "1"}, ] write_gender_counts(destination, rows) assert destination.read_text() == "gender,minifig_count\nmale,2\nfemale,1\n" def test_write_character_variations_totals_outputs_csv(tmp_path: Path) -> None: """Écrit le CSV comparatif variations/total.""" destination = tmp_path / "variations.csv" rows = [ {"known_character": "A", "gender": "male", "variation_count": 2, "total_minifigs": 3}, {"known_character": "B", "gender": "female", "variation_count": 1, "total_minifigs": 1}, ] write_character_variations_totals(destination, rows) assert destination.read_text() == "known_character,gender,variation_count,total_minifigs\nA,male,2,3\nB,female,1,1\n" def test_write_character_gender_counts_outputs_csv(tmp_path: Path) -> None: """Écrit le CSV des comptes de personnages par genre.""" destination = tmp_path / "character_gender.csv" rows = [ {"gender": "female", "character_count": "2"}, {"gender": "male", "character_count": "3"}, ] write_character_gender_counts(destination, rows) assert destination.read_text() == "gender,character_count\nfemale,2\nmale,3\n" def test_write_new_characters_by_year_outputs_csv(tmp_path: Path) -> None: """Écrit le CSV des nouveaux personnages par année.""" destination = tmp_path / "new_characters.csv" rows = [ {"year": "2015", "new_characters": "3"}, {"year": "2016", "new_characters": "1"}, ] write_new_characters_by_year(destination, rows) assert destination.read_text() == "year,new_characters\n2015,3\n2016,1\n" def test_write_new_character_sets_markdown_outputs_md(tmp_path: Path) -> None: """Écrit le Markdown listant les nouveaux personnages et leurs sets.""" destination = tmp_path / "new_characters.md" rows = [ { "year": "2015", "known_character": "Owen Grady", "set_num": "123-1", "set_id": "123", "set_name": "Set A", "rebrickable_url": "http://r/123-1", }, { "year": "2016", "known_character": "Ellie Sattler", "set_num": "125-1", "set_id": "125", "set_name": "Set C", "rebrickable_url": "http://r/125-1", }, ] write_new_character_sets_markdown(destination, rows) assert destination.read_text() == ( "##### 2015\n\n" "- Owen Grady\n" " - [123](http://r/123-1) - Set A\n" "\n" "##### 2016\n\n" "- Ellie Sattler\n" " - [125](http://r/125-1) - Set C\n" "\n" ) def test_aggregate_presence_by_year_excludes_figurants(tmp_path: Path) -> None: """Calcule le total annuel en excluant les figurants.""" sets_path = tmp_path / "sets_enriched.csv" sets_path.write_text( "set_num,year\n" "123-1,2020\n" "124-1,2021\n" ) minifigs_rows = [ { "set_num": "123-1", "known_character": "Owen Grady", "fig_num": "fig-owen", "part_num": "head-a", "gender": "male", }, { "set_num": "124-1", "known_character": "Figurant", "fig_num": "fig-guard", "part_num": "head-b", "gender": "unknown", }, ] sets_years = load_sets_enriched(sets_path) presence = aggregate_presence_by_year(minifigs_rows, sets_years, excluded_characters=["Figurant"]) assert presence == [ {"known_character": "Owen Grady", "year": "2020", "minifig_count": "1"}, {"known_character": "Owen Grady", "year": "2021", "minifig_count": "0"}, ] def test_aggregate_character_spans_excludes_figurants(tmp_path: Path) -> None: """Calcule les bornes min/max par personnage.""" sets_path = tmp_path / "sets_enriched.csv" sets_path.write_text( "set_num,year\n" "123-1,2020\n" "124-1,2021\n" "125-1,2022\n" ) sets_years = load_sets_enriched(sets_path) minifigs_rows = [ { "set_num": "123-1", "known_character": "Owen Grady", "fig_num": "fig-owen", "part_num": "head-a", "gender": "male", }, { "set_num": "124-1", "known_character": "Owen Grady", "fig_num": "fig-owen", "part_num": "head-a", "gender": "male", }, { "set_num": "125-1", "known_character": "Figurant", "fig_num": "fig-guard", "part_num": "head-b", "gender": "unknown", }, ] spans = aggregate_character_spans(minifigs_rows, sets_years, excluded_characters=["Figurant"]) assert spans == [ {"known_character": "Owen Grady", "start_year": "2020", "end_year": "2021", "total_minifigs": "2", "gender": "male"}, ]