From 6c7e81e503c823e03da6eacb54ac48a50bbfe6ef Mon Sep 17 00:00:00 2001 From: Buried-In-Code <6057651+Buried-In-Code@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:27:33 +1300 Subject: [PATCH] Add more tests --- tests/comic/comic_test.py | 81 ++++++++++++++++++++ tests/comic/{archives => }/conftest.py | 0 tests/comic/metadata/__init__.py | 0 tests/comic/metadata/base_test.py | 38 ++++++++++ tests/comic/metadata/comic_info_test.py | 96 ++++++++++++++++++++++++ tests/comic/metadata/metron_info_test.py | 86 +++++++++++++++++++++ tests/conftest.py | 14 ++++ 7 files changed, 315 insertions(+) create mode 100644 tests/comic/comic_test.py rename tests/comic/{archives => }/conftest.py (100%) create mode 100644 tests/comic/metadata/__init__.py create mode 100644 tests/comic/metadata/base_test.py create mode 100644 tests/comic/metadata/comic_info_test.py create mode 100644 tests/comic/metadata/metron_info_test.py create mode 100644 tests/conftest.py diff --git a/tests/comic/comic_test.py b/tests/comic/comic_test.py new file mode 100644 index 0000000..432889d --- /dev/null +++ b/tests/comic/comic_test.py @@ -0,0 +1,81 @@ +from unittest.mock import MagicMock + +import pytest + +from perdoo.cli.process import generate_naming +from perdoo.comic.archives import CBTArchive, CBZArchive +from perdoo.comic.comic import Comic +from perdoo.comic.metadata import ComicInfo, MetronInfo +from perdoo.settings import Naming + + +@pytest.fixture +def cbz_comic(cbz_archive: CBZArchive) -> Comic: + return Comic(filepath=cbz_archive.filepath) + + +@pytest.fixture +def cbt_comic(cbt_archive: CBTArchive) -> Comic: + return Comic(filepath=cbt_archive.filepath) + + +def test_convert_to_cbz(cbt_comic: Comic) -> None: + cbt_comic.convert_to(extension="cbz") + assert isinstance(cbt_comic.archive, CBZArchive) + assert cbt_comic.filepath.suffix == ".cbz" + + +def test_convert_to_cbt(cbz_comic: Comic) -> None: + cbz_comic.convert_to(extension="cbt") + assert isinstance(cbz_comic.archive, CBTArchive) + assert cbz_comic.filepath.suffix == ".cbt" + + +def test_clean_archive(cbz_comic: Comic) -> None: + cbz_comic.archive.list_filenames = MagicMock( + return_value=["001.jpg", "info.txt", "ComicInfo.xml", "cover.png"] + ) + cbz_comic.archive.delete_file = MagicMock() + with cbz_comic.open_session() as session: + for extra in cbz_comic.list_extras(): + session.delete(filename=extra.name) + cbz_comic.archive.delete_file.assert_called_once_with(filename="info.txt") + + +def test_write_comicinfo(cbz_comic: Comic, comic_info: ComicInfo) -> None: + with cbz_comic.open_session() as session: + _, info = cbz_comic.read_metadata(session=session) + assert info is None + session.write(filename=ComicInfo.FILENAME, data=comic_info.to_bytes()) + _, info = cbz_comic.read_metadata(session=session) + assert info == comic_info + + +def test_write_metroninfo(cbz_comic: Comic, metron_info: MetronInfo) -> None: + with cbz_comic.open_session() as session: + info, _ = cbz_comic.read_metadata(session=session) + assert info is None + session.write(filename=MetronInfo.FILENAME, data=metron_info.to_bytes()) + info, _ = cbz_comic.read_metadata(session=session) + assert info == metron_info + + +def test_write_metadata_override(cbz_comic: Comic, metron_info: MetronInfo) -> None: + metadata_copy = metron_info.model_copy(deep=True) + metadata_copy.series.volume = 2 + + with cbz_comic.open_session() as session: + info, _ = cbz_comic.read_metadata(session=session) + assert info is None + session.write(filename=MetronInfo.FILENAME, data=metron_info.to_bytes()) + info, _ = cbz_comic.read_metadata(session=session) + assert info == metron_info + session.write(filename=MetronInfo.FILENAME, data=metadata_copy.to_bytes()) + info, _ = cbz_comic.read_metadata(session=session) + assert info == metadata_copy + + +def test_rename(cbz_comic: Comic, metron_info: MetronInfo) -> None: + naming = generate_naming(settings=Naming(), metron_info=metron_info, comic_info=None) + cbz_comic.move_to(naming=naming, output_folder=cbz_comic.filepath.parent) + assert cbz_comic.filepath.name == "Test-Series-v1_#.cbz" diff --git a/tests/comic/archives/conftest.py b/tests/comic/conftest.py similarity index 100% rename from tests/comic/archives/conftest.py rename to tests/comic/conftest.py diff --git a/tests/comic/metadata/__init__.py b/tests/comic/metadata/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/comic/metadata/base_test.py b/tests/comic/metadata/base_test.py new file mode 100644 index 0000000..fe2a1a7 --- /dev/null +++ b/tests/comic/metadata/base_test.py @@ -0,0 +1,38 @@ +from pathlib import Path + +from perdoo.comic.metadata import ComicInfo +from perdoo.comic.metadata._base import sanitize +from perdoo.settings import Naming + + +def test_sanitize_basic() -> None: + assert sanitize(value="Example Title!", seperator="-") == "Example-Title!" + assert sanitize(value="Example/Title: 123", seperator="-") == "ExampleTitle-123" + assert sanitize(value=" already spaced ", seperator="_") == "already_spaced" + + +def test_sanitize_none_and_empty() -> None: + assert sanitize(value=None, seperator="-") is None + assert sanitize(value="", seperator="-") == "" + + +def test_evaluate_pattern() -> None: + obj = ComicInfo(series="Series", volume=1, number=2, format="Single Issue", publisher="Pub") + settings = Naming(seperator="-", default="{series-name}-{unknown}-{number:03}") + name = obj.get_filename(settings=settings) + + assert name == "Series-unknown-002" + + +def test_metadata_to_file(tmp_path: Path) -> None: + obj = ComicInfo(series="Example", number=1, format="Single Issue") + out = tmp_path / obj.FILENAME + obj.to_file(file=out) + + assert out.exists() + + loaded = ComicInfo.from_bytes(content=out.read_bytes()) + + assert loaded.series == "Example" + assert loaded.number == "1" + assert loaded.format == "Single Issue" diff --git a/tests/comic/metadata/comic_info_test.py b/tests/comic/metadata/comic_info_test.py new file mode 100644 index 0000000..b640ac6 --- /dev/null +++ b/tests/comic/metadata/comic_info_test.py @@ -0,0 +1,96 @@ +from datetime import date + +from perdoo.comic.metadata import ComicInfo +from perdoo.comic.metadata.comic_info import Page, PageType +from perdoo.settings import Naming + + +def test_cover_date_property(comic_info: ComicInfo) -> None: + assert comic_info.cover_date is None + + comic_info.cover_date = date(2020, 2, 29) + assert comic_info.year == 2020 + assert comic_info.month == 2 + assert comic_info.day == 29 + assert comic_info.cover_date == date(2020, 2, 29) + + comic_info.year = None + assert comic_info.year is None + assert comic_info.month == 2 + assert comic_info.day == 29 + assert comic_info.cover_date is None + + +def test_page_ordering_and_hashing() -> None: + p1 = Page(image=2, type=PageType.STORY) + p2 = Page(image=1, type=PageType.FRONT_COVER) + p3 = Page(image=2, type=PageType.OTHER) + + assert sorted([p1, p2]) == [p2, p1] + assert p1 == p3 + assert len({p1, p2, p3}) == 2 + + +def test_credits_property(comic_info: ComicInfo) -> None: + comic_info.credits = { + "Alice": ["Writer"], + "Bob": ["Inker", "Colorist"], + "Charles": ["Writer", "Inker"], + } + + assert comic_info.writer == "Alice,Charles" + assert comic_info.inker == "Bob,Charles" + assert comic_info.colorist == "Bob" + + +def test_list_fields_mapping(comic_info: ComicInfo) -> None: + comic_info.genre_list = ["Sci-Fi", "Noir"] + comic_info.character_list = ["Alice", "Bob"] + comic_info.team_list = ["Team X"] + comic_info.location_list = ["Gotham"] + comic_info.story_arc_list = ["Arc 1", "Arc 2"] + + assert comic_info.genre == "Sci-Fi,Noir" + assert comic_info.characters == "Alice,Bob" + assert comic_info.teams == "Team X" + assert comic_info.locations == "Gotham" + assert comic_info.story_arc == "Arc 1,Arc 2" + + +def test_get_filename_padding_and_sanitization() -> None: + obj = ComicInfo( + publisher="Example Publisher", + series="Example Series", + volume=1, + number=2, + format="Single Issue", + title="Hello: World / ???", + ) + settings = Naming( + seperator="-", + default="{publisher-name}/{series-name}-v{volume}/{series-name}-v{volume}_#{number:02}-{title}", + ) + name = obj.get_filename(settings=settings) + + assert "#02" in name + assert "Hello-World" in name + + +def test_xml_bytes_preserves_pages() -> None: + obj = ComicInfo( + series="Series", + number=1, + format="Single Issue", + pages=[ + Page(image=1, type=PageType.FRONT_COVER, image_size=123), + Page(image=2, type=PageType.STORY, image_size=456), + ], + ) + loaded = ComicInfo.from_bytes(content=obj.to_bytes()) + + assert loaded.series == obj.series + assert len(loaded.pages) == len(obj.pages) + assert loaded.pages[0].image == obj.pages[0].image + assert loaded.pages[0].type == obj.pages[0].type + assert loaded.pages[1].image == obj.pages[1].image + assert loaded.pages[1].type == obj.pages[1].type diff --git a/tests/comic/metadata/metron_info_test.py b/tests/comic/metadata/metron_info_test.py new file mode 100644 index 0000000..8cb2b71 --- /dev/null +++ b/tests/comic/metadata/metron_info_test.py @@ -0,0 +1,86 @@ +from datetime import date, datetime + +import pytest + +from perdoo.comic.metadata import MetronInfo +from perdoo.comic.metadata.metron_info import ( + GTIN, + Credit, + Format, + Id, + InformationSource, + Publisher, + Resource, + Role, +) +from perdoo.settings import Naming + + +def test_information_source_load_valid() -> None: + assert InformationSource.load(value="Metron") is InformationSource.METRON + assert InformationSource.load(value="Comic Vine") is InformationSource.COMIC_VINE + with pytest.raises(ValueError, match=r"isn't a valid InformationSource"): + InformationSource.load(value="Not Real") + + +def test_role_load_unknown_fallback() -> None: + assert Role.load(value="Writer") is Role.WRITER + assert Role.load(value="Not Real") is Role.OTHER + + +def test_ensure_timezone_adds_local_tz(metron_info: MetronInfo) -> None: + naive = datetime(2020, 1, 1, 12, 0, 0) # tzinfo=None + metron_info.last_modified = naive + + assert metron_info.last_modified is not None + assert metron_info.last_modified.tzinfo is not None + + +def test_get_filename_padding_and_sanitization(metron_info: MetronInfo) -> None: + metron_info.number = 2 + metron_info.cover_date = date(2021, 7, 4) + metron_info.ids = [ + Id(primary=False, source=InformationSource.METRON, value="abc"), + Id(primary=True, source=InformationSource.COMIC_VINE, value="cv-123"), + ] + settings = Naming( + seperator="-", + default="{publisher-name}/{series-name}-v{volume}/{series-name}-v{volume}_#{number:03}_{cover-year}_{id}", + ) + name = metron_info.get_filename(settings=settings) + + assert "#002" in name + assert "2021" in name + assert "cv-123" in name + + +def test_pattern_map(metron_info: MetronInfo) -> None: + metron_info.publisher = Publisher(name="Pub", imprint=Resource(value="Imprint Name")) + metron_info.gtin = GTIN(isbn="9781234567890", upc="012345678905") + settings = Naming(seperator="-", default="{publisher-name}/{imprint}/{isbn}/{upc}") + name = metron_info.get_filename(settings=settings) + + assert "Pub" in name + assert "Imprint-Name" in name + assert "9781234567890" in name + assert "012345678905" in name + + +def test_xml_bytes_preserved(metron_info: MetronInfo) -> None: + metron_info.credits = [ + Credit( + creator=Resource(value="Alice"), + roles=[Resource(value=Role.WRITER), Resource(value=Role.INKER)], + ) + ] + metron_info.series.format = Format.SINGLE_ISSUE + metron_info.number = 2 + loaded = MetronInfo.from_bytes(content=metron_info.to_bytes()) + + assert loaded.series.name == metron_info.series.name + assert loaded.series.format == metron_info.series.format + assert loaded.number == metron_info.number + assert loaded.credits[0].creator.value == metron_info.credits[0].creator.value + assert {r.value for r in loaded.credits[0].roles} == { + r.value for r in metron_info.credits[0].roles + } diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e2dc5fa --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ +import pytest + +from perdoo.comic.metadata import ComicInfo, MetronInfo +from perdoo.comic.metadata.metron_info import Series + + +@pytest.fixture +def metron_info() -> MetronInfo: + return MetronInfo(series=Series(name="Test Series")) + + +@pytest.fixture +def comic_info() -> ComicInfo: + return ComicInfo()