diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4776211 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..126dc67 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + pip install poetry + poetry install --no-interaction --no-root --with dev + + - name: Run tests with pytest + run: | + poetry run pytest tests/ -v --cov=cbz --cov-report=xml --cov-report=term-missing diff --git a/pyproject.toml b/pyproject.toml index 4722c24..1ac1e02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,16 @@ license = "MIT" authors = ["hyugogirubato <65763543+hyugogirubato@users.noreply.github.com>"] readme = "README.md" repository = "https://github.com/hyugogirubato/cbz" -keywords = ["python", "cbz", "cbr", "pdf", "ebooks", "manga", "comics", "webtoons"] +keywords = [ + "python", + "cbz", + "cbr", + "pdf", + "ebooks", + "manga", + "comics", + "webtoons", +] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -23,7 +32,7 @@ classifiers = [ include = [ { path = "CHANGELOG.md", format = "sdist" }, { path = "README.md", format = "sdist" }, - { path = "LICENSE", format = "sdist" } + { path = "LICENSE", format = "sdist" }, ] [tool.poetry.urls] @@ -46,5 +55,16 @@ name = "localpypi" url = "https://pypi.org/simple/" priority = "primary" + +[tool.poetry.group.dev.dependencies] +pytest = ">=7.4.0,<8.0.0" +pytest-cov = ">=5.0.0" + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = [ + "--cov=cbz", +] + [certificates] localpypi = { cert = false } diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8e6233d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,49 @@ +import pytest +from pathlib import Path +from cbz.comic import ComicInfo +from cbz.page import PageInfo +from cbz.constants import PageType + +@pytest.fixture +def fixtures_dir(): + """Fixture that provides the path to the test fixtures directory.""" + return Path(__file__).parent / "fixtures" + +@pytest.fixture +def images_dir(fixtures_dir): + """Fixture that provides the path to the test images directory.""" + return fixtures_dir / "images" + +@pytest.fixture +def sample_image_path(images_dir): + """Fixture that provides a sample image path.""" + return images_dir / "page-000.jpg" + +@pytest.fixture +def sample_cbz_file(tmp_path, images_dir): + """Fixture that creates a sample CBZ file for testing.""" + # Load sample pages + image_paths = sorted(list(images_dir.iterdir()))[:3] # Use first 3 images + pages = [] + + for i, path in enumerate(image_paths): + page_type = PageType.FRONT_COVER if i == 0 else PageType.STORY + page = PageInfo.load(path=path, type=page_type) + pages.append(page) + + # Create comic from pages + comic = ComicInfo.from_pages( + pages=pages, + title="Test Comic", + series="Test Series", + number=1, + volume=1, + year=2024 + ) + + # Save to temporary file + cbz_path = tmp_path / "test_comic.cbz" + cbz_content = comic.pack() + cbz_path.write_bytes(cbz_content) + + return cbz_path \ No newline at end of file diff --git a/tests/fixtures/images/page-000.jpg b/tests/fixtures/images/page-000.jpg new file mode 100644 index 0000000..19b675b Binary files /dev/null and b/tests/fixtures/images/page-000.jpg differ diff --git a/tests/fixtures/images/page-001.jpg b/tests/fixtures/images/page-001.jpg new file mode 100644 index 0000000..c40b1d3 Binary files /dev/null and b/tests/fixtures/images/page-001.jpg differ diff --git a/tests/fixtures/images/page-002.jpg b/tests/fixtures/images/page-002.jpg new file mode 100644 index 0000000..6a85c4f Binary files /dev/null and b/tests/fixtures/images/page-002.jpg differ diff --git a/tests/fixtures/images/page-003.jpg b/tests/fixtures/images/page-003.jpg new file mode 100644 index 0000000..23e6547 Binary files /dev/null and b/tests/fixtures/images/page-003.jpg differ diff --git a/tests/fixtures/images/page-004.jpg b/tests/fixtures/images/page-004.jpg new file mode 100644 index 0000000..2e1c2d1 Binary files /dev/null and b/tests/fixtures/images/page-004.jpg differ diff --git a/tests/fixtures/images/page-005.jpg b/tests/fixtures/images/page-005.jpg new file mode 100644 index 0000000..bbeb8bc Binary files /dev/null and b/tests/fixtures/images/page-005.jpg differ diff --git a/tests/fixtures/images/page-006.jpg b/tests/fixtures/images/page-006.jpg new file mode 100644 index 0000000..1d002db Binary files /dev/null and b/tests/fixtures/images/page-006.jpg differ diff --git a/tests/fixtures/images/page-007.jpg b/tests/fixtures/images/page-007.jpg new file mode 100644 index 0000000..9f2e2c5 Binary files /dev/null and b/tests/fixtures/images/page-007.jpg differ diff --git a/tests/fixtures/images/page-008.jpg b/tests/fixtures/images/page-008.jpg new file mode 100644 index 0000000..a7bd0e3 Binary files /dev/null and b/tests/fixtures/images/page-008.jpg differ diff --git a/tests/fixtures/images/page-009.jpg b/tests/fixtures/images/page-009.jpg new file mode 100644 index 0000000..a835f60 Binary files /dev/null and b/tests/fixtures/images/page-009.jpg differ diff --git a/tests/fixtures/images/page-010.jpg b/tests/fixtures/images/page-010.jpg new file mode 100644 index 0000000..b93badd Binary files /dev/null and b/tests/fixtures/images/page-010.jpg differ diff --git a/tests/test_comic.py b/tests/test_comic.py new file mode 100644 index 0000000..7d69eb0 --- /dev/null +++ b/tests/test_comic.py @@ -0,0 +1,155 @@ +from cbz.comic import ComicInfo +from cbz.page import PageInfo +from cbz.constants import PageType, YesNo, Manga, AgeRating, Format + +class TestComicInfo: + """Test cases for ComicInfo class.""" + + def test_from_pages_creation(self, images_dir): + """Test creating ComicInfo from pages.""" + # Load sample pages + image_paths = sorted(list(images_dir.iterdir()))[:3] # Use first 3 images + pages = [] + + for i, path in enumerate(image_paths): + page_type = PageType.FRONT_COVER if i == 0 else PageType.STORY + page = PageInfo.load(path=path, type=page_type) + pages.append(page) + + # Create comic from pages + comic = ComicInfo.from_pages( + pages=pages, + title="Test Comic", + series="Test Series", + number=1, + volume=1, + year=2024 + ) + + assert comic.title == "Test Comic" + assert comic.series == "Test Series" + assert comic.number == 1 + assert comic.volume == 1 + assert comic.year == 2024 + assert len(comic.pages) == 3 + assert comic.pages[0].type == PageType.FRONT_COVER + assert comic.pages[1].type == PageType.STORY + + def test_from_cbz_file(self, sample_cbz_file): + """Test loading ComicInfo from CBZ file.""" + comic = ComicInfo.from_cbz(sample_cbz_file) + + assert comic is not None + assert hasattr(comic, 'pages') + assert len(comic.pages) > 0 + assert all(isinstance(page, PageInfo) for page in comic.pages) + + def test_pack_cbz(self, images_dir): + """Test packing comic into CBZ format.""" + # Create a simple comic + image_paths = sorted(list(images_dir.iterdir()))[:2] + pages = [PageInfo.load(path=path) for path in image_paths] + + comic = ComicInfo.from_pages( + pages=pages, + title="Pack Test", + series="Test Series" + ) + + # Pack to CBZ + cbz_content = comic.pack() + + assert isinstance(cbz_content, bytes) + assert len(cbz_content) > 0 + + def test_pack_with_rename(self, images_dir): + """Test packing comic with page renaming.""" + image_paths = sorted(list(images_dir.iterdir()))[:2] + pages = [PageInfo.load(path=path) for path in image_paths] + + comic = ComicInfo.from_pages( + pages=pages, + title="Rename Test" + ) + + # Pack with rename option + cbz_content = comic.pack(rename=True) + + assert isinstance(cbz_content, bytes) + assert len(cbz_content) > 0 + + def test_comic_metadata_properties(self, images_dir): + """Test comic metadata properties.""" + image_paths = sorted(list(images_dir.iterdir()))[:1] + pages = [PageInfo.load(path=path) for path in image_paths] + + comic = ComicInfo.from_pages( + pages=pages, + title="Metadata Test", + series="Test Series", + number=5, + count=10, + volume=2, + summary="Test summary", + year=2023, + month=6, + day=15, + writer="Test Writer", + penciller="Test Penciller", + inker="Test Inker", + colorist="Test Colorist", + letterer="Test Letterer", + cover_artist="Test Cover Artist", + editor="Test Editor", + publisher="Test Publisher", + imprint="Test Imprint", + genre="Test Genre", + language_iso="en", + format=Format.SERIES, + black_white=YesNo.NO, + manga=Manga.RIGHT_LEFT, + age_rating=AgeRating.TEEN, + community_rating=4 + ) + + assert comic.title == "Metadata Test" + assert comic.series == "Test Series" + assert comic.number == 5 + assert comic.count == 10 + assert comic.volume == 2 + assert comic.summary == "Test summary" + assert comic.year == 2023 + assert comic.month == 6 + assert comic.day == 15 + assert comic.writer == "Test Writer" + assert comic.penciller == "Test Penciller" + assert comic.inker == "Test Inker" + assert comic.colorist == "Test Colorist" + assert comic.letterer == "Test Letterer" + assert comic.cover_artist == "Test Cover Artist" + assert comic.editor == "Test Editor" + assert comic.publisher == "Test Publisher" + assert comic.imprint == "Test Imprint" + assert comic.genre == "Test Genre" + assert comic.language_iso == "en" + assert comic.format == Format.SERIES + assert comic.black_white == YesNo.NO + assert comic.manga == Manga.RIGHT_LEFT + assert comic.age_rating == AgeRating.TEEN + assert comic.community_rating == 4 + + def test_page_count_property(self, images_dir): + """Test that page count returns correct count.""" + image_paths = sorted(list(images_dir.iterdir()))[:4] + pages = [PageInfo.load(path=path) for path in image_paths] + + comic = ComicInfo.from_pages(pages=pages, title="Count Test") + + assert len(comic.pages) == 4 + + def test_empty_pages_list(self): + """Test creating comic with empty pages list.""" + comic = ComicInfo.from_pages(pages=[], title="Empty Test") + + assert comic.title == "Empty Test" + assert len(comic.pages) == 0 \ No newline at end of file diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..cec09de --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,269 @@ +from cbz.models import BaseModel, ComicModel, PageModel +from cbz.constants import Format, YesNo, Manga, AgeRating, PageType + +class TestBaseModel: + """Test cases for BaseModel class.""" + + def test_base_model_creation(self): + """Test creating BaseModel with fields.""" + test_fields = { + 'test_str': ('Test String', str), + 'test_int': ('Test Integer', int), + 'test_bool': ('Test Boolean', bool) + } + + model = BaseModel(fields=test_fields) + + # Check default values are set + assert hasattr(model, 'test_str') + assert hasattr(model, 'test_int') + assert hasattr(model, 'test_bool') + + def test_base_model_with_kwargs(self): + """Test creating BaseModel with keyword arguments.""" + test_fields = { + 'title': ('Title', str), + 'number': ('Number', int), + 'published': ('Published', bool) + } + + model = BaseModel( + fields=test_fields, + title="Test Title", + number=42, + published=True + ) + + assert model.title == "Test Title" + assert model.number == 42 + assert model.published + + def test_attribute_type_verification(self): + """Test that attribute types are verified on assignment.""" + test_fields = { + 'count': ('Count', int), + 'name': ('Name', str) + } + + model = BaseModel(fields=test_fields) + + # Valid assignments + model.count = 10 + model.name = "Test" + + assert model.count == 10 + assert model.name == "Test" + + def test_repr_method(self): + """Test string representation of BaseModel.""" + test_fields = { + 'title': ('Title', str) + } + + model = BaseModel(fields=test_fields, title="Test") + repr_str = repr(model) + + assert isinstance(repr_str, str) + assert "BaseModel" in repr_str + + +class TestComicModel: + """Test cases for ComicModel class.""" + + def test_comic_model_creation(self): + """Test creating ComicModel with default values.""" + model = ComicModel() + + # Check that comic-specific attributes exist + assert hasattr(model, 'title') + assert hasattr(model, 'series') + assert hasattr(model, 'number') + assert hasattr(model, 'volume') + assert hasattr(model, 'year') + assert hasattr(model, 'month') + assert hasattr(model, 'day') + + def test_comic_model_with_values(self): + """Test creating ComicModel with specific values.""" + model = ComicModel( + title="Test Comic", + series="Test Series", + number=1, + volume=1, + year=2024, + month=6, + day=15, + writer="Test Writer", + publisher="Test Publisher", + language_iso="en", + format=Format.SERIES, + black_white=YesNo.NO, + manga=Manga.RIGHT_LEFT, + age_rating=AgeRating.EVERYONE + ) + + assert model.title == "Test Comic" + assert model.series == "Test Series" + assert model.number == 1 + assert model.volume == 1 + assert model.year == 2024 + assert model.month == 6 + assert model.day == 15 + assert model.writer == "Test Writer" + assert model.publisher == "Test Publisher" + assert model.language_iso == "en" + assert model.format == Format.SERIES + assert model.black_white == YesNo.NO + assert model.manga == Manga.RIGHT_LEFT + assert model.age_rating == AgeRating.EVERYONE + + def test_comic_model_enum_properties(self): + """Test that enum properties work correctly.""" + model = ComicModel() + + # Test format enum + model.format = Format.PREVIEW + assert model.format == Format.PREVIEW + + # Test yes/no enum + model.black_white = YesNo.YES + assert model.black_white == YesNo.YES + + # Test manga enum + model.manga = Manga.RIGHT_LEFT + assert model.manga == Manga.RIGHT_LEFT + + # Test age rating enum + model.age_rating = AgeRating.TEEN + assert model.age_rating == AgeRating.TEEN + + def test_comic_model_metadata_fields(self): + """Test comic metadata fields.""" + model = ComicModel( + summary="Test summary", + notes="Test notes", + genre="Adventure", + web="http://example.com", + ean="1234567890123", + community_rating=5, + main_character_or_team="Hero", + characters="Hero, Villain", + teams="Justice League", + locations="Metropolis", + scan_information="Scanned by Test", + story_arc="Origin Story", + series_group="DC Comics", + alternate_series="Alternate Universe", + alternate_number=2, + alternate_count=10 + ) + + assert model.summary == "Test summary" + assert model.notes == "Test notes" + assert model.genre == "Adventure" + assert model.web == "http://example.com" + assert model.ean == "1234567890123" + assert model.community_rating == 5 + assert model.main_character_or_team == "Hero" + assert model.characters == "Hero, Villain" + assert model.teams == "Justice League" + assert model.locations == "Metropolis" + assert model.scan_information == "Scanned by Test" + assert model.story_arc == "Origin Story" + assert model.series_group == "DC Comics" + assert model.alternate_series == "Alternate Universe" + assert model.alternate_number == 2 + assert model.alternate_count == 10 + + +class TestPageModel: + """Test cases for PageModel class.""" + + def test_page_model_creation(self): + """Test creating PageModel with default values.""" + model = PageModel() + + # Check that page-specific attributes exist + assert hasattr(model, 'image') + assert hasattr(model, 'type') + assert hasattr(model, 'double') + assert hasattr(model, 'image_size') + assert hasattr(model, 'key') + assert hasattr(model, 'bookmark') + assert hasattr(model, 'image_width') + assert hasattr(model, 'image_height') + assert hasattr(model, 'image_size') + # Note: format is not a base field in PageModel + + def test_page_model_with_values(self): + """Test creating PageModel with specific values.""" + model = PageModel( + image=1, + type=PageType.FRONT_COVER, + double=True, + image_size=1024000, + key="cover", + bookmark="Chapter 1", + image_width=800, + image_height=1200, + ) + + assert model.image == 1 + assert model.type == PageType.FRONT_COVER + assert model.double + assert model.image_size == 1024000 + assert model.key == "cover" + assert model.bookmark == "Chapter 1" + assert model.image_width == 800 + assert model.image_height == 1200 + + def test_page_model_page_types(self): + """Test different page types.""" + page_types = [ + PageType.FRONT_COVER, + PageType.INNER_COVER, + PageType.ROUNDUP, + PageType.STORY, + PageType.ADVERTISEMENT, + PageType.EDITORIAL, + PageType.LETTERS, + PageType.PREVIEW, + PageType.BACK_COVER, + PageType.OTHER, + PageType.DELETED + ] + + for page_type in page_types: + model = PageModel(type=page_type) + assert model.type == page_type + + def test_page_model_boolean_properties(self): + """Test boolean properties in PageModel.""" + model = PageModel() + + # Test double property + model.double = True + assert model.double + + model.double = False + assert not model.double + + def test_page_model_numeric_properties(self): + """Test numeric properties in PageModel.""" + model = PageModel( + image=5, + image_size=2048000, + image_width=1920, + image_height=1080 + ) + + assert model.image == 5 + assert model.image_size == 2048000 + assert model.image_width == 1920 + assert model.image_height == 1080 + + # Test that they're integers + assert isinstance(model.image, int) + assert isinstance(model.image_size, int) + assert isinstance(model.image_width, int) + assert isinstance(model.image_height, int) \ No newline at end of file diff --git a/tests/test_page.py b/tests/test_page.py new file mode 100644 index 0000000..b4763d3 --- /dev/null +++ b/tests/test_page.py @@ -0,0 +1,139 @@ +import pytest +from cbz.page import PageInfo +from cbz.constants import PageType + +class TestPageInfo: + """Test cases for PageInfo class.""" + + def test_load_from_file(self, sample_image_path): + """Test loading PageInfo from image file.""" + page = PageInfo.load(path=sample_image_path) + + assert page is not None + assert isinstance(page.content, bytes) + assert len(page.content) > 0 + assert page.name == sample_image_path.name + assert page.image_width > 0 + assert page.image_height > 0 + assert page.image_size > 0 + assert page.suffix is not None + + def test_load_with_page_type(self, sample_image_path): + """Test loading PageInfo with specific page type.""" + page = PageInfo.load(path=sample_image_path, type=PageType.FRONT_COVER) + + assert page.type == PageType.FRONT_COVER + + def test_load_with_custom_name(self, sample_image_path): + """Test loading PageInfo with custom name.""" + custom_name = "custom_page.jpg" + page = PageInfo.load(path=sample_image_path, name=custom_name) + + assert page.name == custom_name + + def test_page_content_property(self, sample_image_path): + """Test page content property getter and setter.""" + page = PageInfo.load(path=sample_image_path) + original_content = page.content + + # Test getter + assert page.content == original_content + assert isinstance(page.content, bytes) + + # Test that content is properly set and metadata extracted + assert page.image_width > 0 + assert page.image_height > 0 + assert page.image_size > 0 + + def test_image_metadata_extraction(self, sample_image_path): + """Test that image metadata is correctly extracted.""" + page = PageInfo.load(path=sample_image_path) + + # Check that all image metadata properties are set + assert hasattr(page, 'image_width') and page.image_width > 0 + assert hasattr(page, 'image_height') and page.image_height > 0 + assert hasattr(page, 'image_size') and page.image_size > 0 + assert hasattr(page, 'suffix') and page.suffix is not None + + # Verify dimensions make sense for an image + assert isinstance(page.image_width, int) + assert isinstance(page.image_height, int) + assert isinstance(page.image_size, int) + + def test_multiple_image_formats(self, images_dir): + """Test loading different image formats.""" + image_files = list(images_dir.glob("*.jpg")) + + if not image_files: + pytest.skip("No image files found in example directory") + + for image_path in image_files[:3]: # Test first 3 images + page = PageInfo.load(path=image_path) + + assert page is not None + assert isinstance(page.content, bytes) + assert len(page.content) > 0 + assert page.image_width > 0 + assert page.image_height > 0 + assert page.image_size > 0 + + def test_page_type_assignment(self, sample_image_path): + """Test different page type assignments.""" + page_types = [ + PageType.FRONT_COVER, + PageType.INNER_COVER, + PageType.ROUNDUP, + PageType.STORY, + PageType.ADVERTISEMENT, + PageType.EDITORIAL, + PageType.LETTERS, + PageType.PREVIEW, + PageType.BACK_COVER, + PageType.OTHER, + PageType.DELETED + ] + + for page_type in page_types: + page = PageInfo.load(path=sample_image_path, type=page_type) + assert page.type == page_type + + def test_page_creation_from_bytes(self, sample_image_path): + """Test creating PageInfo directly from bytes.""" + # Read image file as bytes + with open(sample_image_path, 'rb') as f: + image_bytes = f.read() + + # Create page from bytes + page = PageInfo(content=image_bytes, name="test_page.jpg") + + assert page.content == image_bytes + assert page.name == "test_page.jpg" + assert page.image_width > 0 + assert page.image_height > 0 + assert page.image_size > 0 + + def test_repr_string(self, sample_image_path): + """Test string representation of PageInfo.""" + page = PageInfo.load(path=sample_image_path, type=PageType.STORY) + + repr_str = repr(page) + assert "PageInfo" in repr_str + assert isinstance(repr_str, str) + + def test_page_bookmark_property(self, sample_image_path): + """Test page bookmark property.""" + # Test with bookmark + page = PageInfo.load(path=sample_image_path, bookmark="Chapter 1") + assert page.bookmark == "Chapter 1" + + # Test without bookmark + page_no_bookmark = PageInfo.load(path=sample_image_path) + assert hasattr(page_no_bookmark, 'bookmark') + + def test_page_double_page_property(self, sample_image_path): + """Test page double_page property.""" + page = PageInfo.load(path=sample_image_path, double=True) + assert page.double + + page_no_double = PageInfo.load(path=sample_image_path) + assert hasattr(page_no_double, 'double') \ No newline at end of file