mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Converts the conversion into a database migration
This commit is contained in:
		| @@ -1,97 +0,0 @@ | ||||
| import logging | ||||
| import multiprocessing.pool | ||||
| import shutil | ||||
| import tempfile | ||||
| import time | ||||
| from pathlib import Path | ||||
|  | ||||
| from django.core.management.base import BaseCommand | ||||
| from documents.models import Document | ||||
| from documents.parsers import run_convert | ||||
|  | ||||
| logger = logging.getLogger("paperless.management.convert_thumbnails") | ||||
|  | ||||
|  | ||||
| def _do_convert(work_package): | ||||
|     _, existing_thumbnail, converted_thumbnail = work_package | ||||
|     try: | ||||
|  | ||||
|         logger.info(f"Converting thumbnail: {existing_thumbnail}") | ||||
|  | ||||
|         # Run actual conversion | ||||
|         run_convert( | ||||
|             density=300, | ||||
|             scale="500x5000>", | ||||
|             alpha="remove", | ||||
|             strip=True, | ||||
|             trim=False, | ||||
|             auto_orient=True, | ||||
|             input_file=f"{existing_thumbnail}[0]", | ||||
|             output_file=str(converted_thumbnail), | ||||
|         ) | ||||
|  | ||||
|         # Copy newly created thumbnail to thumbnail directory | ||||
|         shutil.copy(converted_thumbnail, existing_thumbnail.parent) | ||||
|  | ||||
|         # Remove the PNG version | ||||
|         existing_thumbnail.unlink() | ||||
|  | ||||
|         logger.info( | ||||
|             "Conversion to WebP completed, " | ||||
|             f"replaced {existing_thumbnail.name} with {converted_thumbnail.name}", | ||||
|         ) | ||||
|  | ||||
|     except Exception as e: | ||||
|         logger.error( | ||||
|             f"Error converting thumbnail" f" (existing file unchanged): {e}", | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|  | ||||
|     help = """ | ||||
|         Converts existing PNG thumbnails into | ||||
|         WebP format. | ||||
|     """.replace( | ||||
|         "    ", | ||||
|         "", | ||||
|     ) | ||||
|  | ||||
|     def handle(self, *args, **options): | ||||
|  | ||||
|         logger.info("Converting all PNG thumbnails to WebP") | ||||
|         start = time.time() | ||||
|         documents = Document.objects.all() | ||||
|  | ||||
|         with tempfile.TemporaryDirectory() as tempdir: | ||||
|  | ||||
|             work_packages = [] | ||||
|  | ||||
|             for document in documents: | ||||
|                 existing_thumbnail = Path(document.thumbnail_path).resolve() | ||||
|  | ||||
|                 if existing_thumbnail.suffix == ".png": | ||||
|  | ||||
|                     # Change the existing filename suffix from png to webp | ||||
|                     converted_thumbnail_name = existing_thumbnail.with_suffix( | ||||
|                         ".webp", | ||||
|                     ).name | ||||
|  | ||||
|                     # Create the expected output filename in the tempdir | ||||
|                     converted_thumbnail = ( | ||||
|                         Path(tempdir) / Path(converted_thumbnail_name) | ||||
|                     ).resolve() | ||||
|  | ||||
|                     # Package up the necessary info | ||||
|                     work_packages.append( | ||||
|                         (document, existing_thumbnail, converted_thumbnail), | ||||
|                     ) | ||||
|  | ||||
|             if len(work_packages): | ||||
|                 with multiprocessing.pool.Pool(processes=4, maxtasksperchild=4) as pool: | ||||
|                     pool.map(_do_convert, work_packages) | ||||
|  | ||||
|             end = time.time() | ||||
|             duration = end - start | ||||
|  | ||||
|         logger.info(f"Conversion completed in {duration:.3f}s") | ||||
							
								
								
									
										107
									
								
								src/documents/migrations/1021_webp_thumbnail_conversion.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/documents/migrations/1021_webp_thumbnail_conversion.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| # Generated by Django 4.0.5 on 2022-06-11 15:40 | ||||
| import logging | ||||
| import multiprocessing.pool | ||||
| import shutil | ||||
| import tempfile | ||||
| import time | ||||
| from pathlib import Path | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.db import migrations | ||||
| from documents.parsers import run_convert | ||||
|  | ||||
| logger = logging.getLogger("paperless.migrations") | ||||
|  | ||||
|  | ||||
| def _do_convert(work_package): | ||||
|     existing_thumbnail, converted_thumbnail = work_package | ||||
|     try: | ||||
|  | ||||
|         logger.info(f"Converting thumbnail: {existing_thumbnail}") | ||||
|  | ||||
|         # Run actual conversion | ||||
|         run_convert( | ||||
|             density=300, | ||||
|             scale="500x5000>", | ||||
|             alpha="remove", | ||||
|             strip=True, | ||||
|             trim=False, | ||||
|             auto_orient=True, | ||||
|             input_file=f"{existing_thumbnail}[0]", | ||||
|             output_file=str(converted_thumbnail), | ||||
|         ) | ||||
|  | ||||
|         # Copy newly created thumbnail to thumbnail directory | ||||
|         shutil.copy(converted_thumbnail, existing_thumbnail.parent) | ||||
|  | ||||
|         # Remove the PNG version | ||||
|         existing_thumbnail.unlink() | ||||
|  | ||||
|         logger.info( | ||||
|             "Conversion to WebP completed, " | ||||
|             f"replaced {existing_thumbnail.name} with {converted_thumbnail.name}", | ||||
|         ) | ||||
|  | ||||
|     except Exception as e: | ||||
|         logger.error(f"Error converting thumbnail (existing file unchanged): {e}") | ||||
|  | ||||
|  | ||||
| def _convert_thumbnails_to_webp(apps, schema_editor): | ||||
|     start = time.time() | ||||
|  | ||||
|     with tempfile.TemporaryDirectory() as tempdir: | ||||
|  | ||||
|         work_packages = [] | ||||
|  | ||||
|         for file in Path(settings.THUMBNAIL_DIR).glob("*.png"): | ||||
|             existing_thumbnail = file.resolve() | ||||
|  | ||||
|             # Change the existing filename suffix from png to webp | ||||
|             converted_thumbnail_name = existing_thumbnail.with_suffix( | ||||
|                 ".webp", | ||||
|             ).name | ||||
|  | ||||
|             # Create the expected output filename in the tempdir | ||||
|             converted_thumbnail = ( | ||||
|                 Path(tempdir) / Path(converted_thumbnail_name) | ||||
|             ).resolve() | ||||
|  | ||||
|             # Package up the necessary info | ||||
|             work_packages.append( | ||||
|                 (existing_thumbnail, converted_thumbnail), | ||||
|             ) | ||||
|  | ||||
|         if len(work_packages): | ||||
|  | ||||
|             logger.info( | ||||
|                 "\n\n" | ||||
|                 "  This is a one-time only migration to convert thumbnails for all of your\n" | ||||
|                 "  documents into WebP format.  If you have a lot of documents though, \n" | ||||
|                 "  this may take a while, so a coffee break may be in order." | ||||
|                 "\n", | ||||
|             ) | ||||
|  | ||||
|             with multiprocessing.pool.Pool( | ||||
|                 processes=min(multiprocessing.cpu_count(), 4), | ||||
|                 maxtasksperchild=4, | ||||
|             ) as pool: | ||||
|                 pool.map(_do_convert, work_packages) | ||||
|  | ||||
|         end = time.time() | ||||
|         duration = end - start | ||||
|  | ||||
|     logger.info(f"Conversion completed in {duration:.3f}s") | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("documents", "1020_merge_20220518_1839"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RunPython( | ||||
|             code=_convert_thumbnails_to_webp, | ||||
|             reverse_code=migrations.RunPython.noop, | ||||
|         ), | ||||
|     ] | ||||
| @@ -294,26 +294,13 @@ class Document(models.Model): | ||||
|  | ||||
|     @property | ||||
|     def thumbnail_path(self) -> str: | ||||
|         png_file_name = f"{self.pk:07}.png" | ||||
|         webp_file_name = f"{self.pk:07}.webp" | ||||
|         if self.storage_type == self.STORAGE_TYPE_GPG: | ||||
|             png_file_name += ".gpg" | ||||
|             webp_file_name += ".gpg" | ||||
|  | ||||
|         # This property is used to both generate the file path | ||||
|         # and locate the file itself | ||||
|         # Hence why this looks a little weird | ||||
|  | ||||
|         webp_file_path = os.path.join(settings.THUMBNAIL_DIR, webp_file_name) | ||||
|         png_file_path = os.path.join(settings.THUMBNAIL_DIR, png_file_name) | ||||
|  | ||||
|         # 1. Assume the thumbnail is WebP | ||||
|         if os.path.exists(png_file_path): | ||||
|             thumb = png_file_path | ||||
|         else: | ||||
|             thumb = webp_file_path | ||||
|  | ||||
|         return os.path.normpath(thumb) | ||||
|         return os.path.normpath(webp_file_path) | ||||
|  | ||||
|     @property | ||||
|     def thumbnail_file(self): | ||||
|   | ||||
| @@ -1,139 +0,0 @@ | ||||
| import filecmp | ||||
| import shutil | ||||
| import tempfile | ||||
| from io import StringIO | ||||
| from pathlib import Path | ||||
| from unittest import mock | ||||
|  | ||||
| from django.core.management import call_command | ||||
| from django.test import override_settings | ||||
| from django.test import TestCase | ||||
| from documents.models import Document | ||||
|  | ||||
|  | ||||
| class TestConvertThumbnails(TestCase): | ||||
|     def call_command(self): | ||||
|         stdout = StringIO() | ||||
|         stderr = StringIO() | ||||
|         call_command( | ||||
|             "convert_thumbnails", | ||||
|             "--no-color", | ||||
|             stdout=stdout, | ||||
|             stderr=stderr, | ||||
|         ) | ||||
|         return stdout.getvalue(), stderr.getvalue() | ||||
|  | ||||
|     def setUp(self): | ||||
|         """ | ||||
|         Creates a document in the database | ||||
|         """ | ||||
|         super().setUp() | ||||
|  | ||||
|         self.doc = Document.objects.create( | ||||
|             pk=1, | ||||
|             checksum="A", | ||||
|             title="A", | ||||
|             content="first document", | ||||
|             mime_type="application/pdf", | ||||
|         ) | ||||
|         self.doc.save() | ||||
|  | ||||
|     def pretend_convert_output(self, *args, **kwargs): | ||||
|         """ | ||||
|         Pretends to do the conversion, by copying the input file | ||||
|         to the output file | ||||
|         """ | ||||
|         shutil.copy2( | ||||
|             Path(kwargs["input_file"].rstrip("[0]")), | ||||
|             Path(kwargs["output_file"]), | ||||
|         ) | ||||
|  | ||||
|     def create_webp_thumbnail_file(self, thumb_dir): | ||||
|         """ | ||||
|         Creates a dummy WebP thumbnail file in the given directory, based on | ||||
|         the database Document | ||||
|         """ | ||||
|         thumb_file = Path(thumb_dir) / Path(f"{self.doc.pk:07}.webp") | ||||
|         thumb_file.write_text("this is a dummy webp file") | ||||
|         return thumb_file | ||||
|  | ||||
|     def create_png_thumbnail_file(self, thumb_dir): | ||||
|         """ | ||||
|         Creates a dummy PNG thumbnail file in the given directory, based on | ||||
|         the database Document | ||||
|         """ | ||||
|         thumb_file = Path(thumb_dir) / Path(f"{self.doc.pk:07}.png") | ||||
|         thumb_file.write_text("this is a dummy png file") | ||||
|         return thumb_file | ||||
|  | ||||
|     @mock.patch("documents.management.commands.convert_thumbnails.run_convert") | ||||
|     def test_do_nothing_if_converted(self, run_convert_mock): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Document exists with default WebP thumbnail path | ||||
|         WHEN: | ||||
|             - Thumbnail conversion is attempted | ||||
|         THEN: | ||||
|             - Nothing is converted | ||||
|         """ | ||||
|  | ||||
|         stdout, _ = self.call_command() | ||||
|         run_convert_mock.assert_not_called() | ||||
|         self.assertIn("Converting all PNG thumbnails to WebP", stdout) | ||||
|  | ||||
|     @mock.patch("documents.management.commands.convert_thumbnails.run_convert") | ||||
|     def test_convert_single_thumbnail(self, run_convert_mock): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Document exists with PNG thumbnail | ||||
|         WHEN: | ||||
|             - Thumbnail conversion is attempted | ||||
|         THEN: | ||||
|             - Single thumbnail is converted | ||||
|         """ | ||||
|  | ||||
|         run_convert_mock.side_effect = self.pretend_convert_output | ||||
|  | ||||
|         with tempfile.TemporaryDirectory() as thumbnail_dir: | ||||
|  | ||||
|             with override_settings( | ||||
|                 THUMBNAIL_DIR=thumbnail_dir, | ||||
|             ): | ||||
|  | ||||
|                 thumb_file = self.create_png_thumbnail_file(thumbnail_dir) | ||||
|  | ||||
|                 stdout, _ = self.call_command() | ||||
|  | ||||
|                 run_convert_mock.assert_called_once() | ||||
|                 self.assertIn(f"{thumb_file}", stdout) | ||||
|                 self.assertIn("Conversion to WebP completed", stdout) | ||||
|  | ||||
|                 self.assertFalse(thumb_file.exists()) | ||||
|                 self.assertTrue(thumb_file.with_suffix(".webp").exists()) | ||||
|  | ||||
|     @mock.patch("documents.management.commands.convert_thumbnails.run_convert") | ||||
|     def test_convert_errors_out(self, run_convert_mock): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Document exists with PNG thumbnail | ||||
|         WHEN: | ||||
|             - Thumbnail conversion is attempted, but raises an exception | ||||
|         THEN: | ||||
|             - Single thumbnail is converted | ||||
|         """ | ||||
|  | ||||
|         run_convert_mock.side_effect = OSError | ||||
|  | ||||
|         with tempfile.TemporaryDirectory() as thumbnail_dir: | ||||
|  | ||||
|             with override_settings( | ||||
|                 THUMBNAIL_DIR=thumbnail_dir, | ||||
|             ): | ||||
|  | ||||
|                 thumb_file = self.create_png_thumbnail_file(thumbnail_dir) | ||||
|  | ||||
|                 _, stderr = self.call_command() | ||||
|  | ||||
|                 run_convert_mock.assert_called_once() | ||||
|                 self.assertIn("Error converting thumbnail", stderr) | ||||
|                 self.assertTrue(thumb_file.exists()) | ||||
							
								
								
									
										231
									
								
								src/documents/tests/test_migration_webp_conversion.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								src/documents/tests/test_migration_webp_conversion.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,231 @@ | ||||
| import shutil | ||||
| import tempfile | ||||
| from pathlib import Path | ||||
| from typing import Callable | ||||
| from typing import Iterable | ||||
| from typing import Union | ||||
| from unittest import mock | ||||
|  | ||||
| from django.test import override_settings | ||||
| from documents.tests.test_migration_archive_files import thumbnail_path | ||||
| from documents.tests.utils import TestMigrations | ||||
|  | ||||
|  | ||||
| @mock.patch( | ||||
|     "documents.migrations.1021_webp_thumbnail_conversion.multiprocessing.pool.Pool.map", | ||||
| ) | ||||
| @mock.patch("documents.migrations.1021_webp_thumbnail_conversion.run_convert") | ||||
| class TestMigrateWebPThumbnails(TestMigrations): | ||||
|  | ||||
|     migrate_from = "1020_merge_20220518_1839" | ||||
|     migrate_to = "1021_webp_thumbnail_conversion" | ||||
|     auto_migrate = False | ||||
|  | ||||
|     def pretend_convert_output(self, *args, **kwargs): | ||||
|         """ | ||||
|         Pretends to do the conversion, by copying the input file | ||||
|         to the output file | ||||
|         """ | ||||
|         shutil.copy2( | ||||
|             Path(kwargs["input_file"].rstrip("[0]")), | ||||
|             Path(kwargs["output_file"]), | ||||
|         ) | ||||
|  | ||||
|     def pretend_map(self, func: Callable, iterable: Iterable): | ||||
|         """ | ||||
|         Pretends to be the map of a multiprocessing.Pool, but secretly does | ||||
|         everything in series | ||||
|         """ | ||||
|         for item in iterable: | ||||
|             func(item) | ||||
|  | ||||
|     def create_dummy_thumbnails( | ||||
|         self, | ||||
|         thumb_dir: Path, | ||||
|         ext: str, | ||||
|         count: int, | ||||
|         start_count: int = 0, | ||||
|     ): | ||||
|         """ | ||||
|         Helper to create a certain count of files of given extension in a given directory | ||||
|         """ | ||||
|         for idx in range(count): | ||||
|             (Path(thumb_dir) / Path(f"{start_count + idx:07}.{ext}")).touch() | ||||
|         # Triple check expected files exist | ||||
|         self.assert_file_count_by_extension(ext, thumb_dir, count) | ||||
|  | ||||
|     def create_webp_thumbnail_files( | ||||
|         self, | ||||
|         thumb_dir: Path, | ||||
|         count: int, | ||||
|         start_count: int = 0, | ||||
|     ): | ||||
|         """ | ||||
|         Creates a dummy WebP thumbnail file in the given directory, based on | ||||
|         the database Document | ||||
|         """ | ||||
|         self.create_dummy_thumbnails(thumb_dir, "webp", count, start_count) | ||||
|  | ||||
|     def create_png_thumbnail_file( | ||||
|         self, | ||||
|         thumb_dir: Path, | ||||
|         count: int, | ||||
|         start_count: int = 0, | ||||
|     ): | ||||
|         """ | ||||
|         Creates a dummy PNG thumbnail file in the given directory, based on | ||||
|         the database Document | ||||
|         """ | ||||
|         self.create_dummy_thumbnails(thumb_dir, "png", count, start_count) | ||||
|  | ||||
|     def assert_file_count_by_extension( | ||||
|         self, | ||||
|         ext: str, | ||||
|         dir: Union[str, Path], | ||||
|         expected_count: int, | ||||
|     ): | ||||
|         """ | ||||
|         Helper to assert a certain count of given extension files in given directory | ||||
|         """ | ||||
|         if not isinstance(dir, Path): | ||||
|             dir = Path(dir) | ||||
|         matching_files = list(dir.glob(f"*.{ext}")) | ||||
|         self.assertEqual(len(matching_files), expected_count) | ||||
|  | ||||
|     def assert_png_file_count(self, dir: Path, expected_count: int): | ||||
|         """ | ||||
|         Helper to assert a certain count of PNG extension files in given directory | ||||
|         """ | ||||
|         self.assert_file_count_by_extension("png", dir, expected_count) | ||||
|  | ||||
|     def assert_webp_file_count(self, dir: Path, expected_count: int): | ||||
|         """ | ||||
|         Helper to assert a certain count of WebP extension files in given directory | ||||
|         """ | ||||
|         self.assert_file_count_by_extension("webp", dir, expected_count) | ||||
|  | ||||
|     def setUp(self): | ||||
|  | ||||
|         self.thumbnail_dir = Path(tempfile.mkdtemp()).resolve() | ||||
|  | ||||
|         return super().setUp() | ||||
|  | ||||
|     def tearDown(self) -> None: | ||||
|  | ||||
|         shutil.rmtree(self.thumbnail_dir) | ||||
|  | ||||
|         return super().tearDown() | ||||
|  | ||||
|     def test_do_nothing_if_converted( | ||||
|         self, | ||||
|         run_convert_mock: mock.MagicMock, | ||||
|         map_mock: mock.MagicMock, | ||||
|     ): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Document exists with default WebP thumbnail path | ||||
|         WHEN: | ||||
|             - Thumbnail conversion is attempted | ||||
|         THEN: | ||||
|             - Nothing is converted | ||||
|         """ | ||||
|         map_mock.side_effect = self.pretend_map | ||||
|  | ||||
|         with override_settings( | ||||
|             THUMBNAIL_DIR=self.thumbnail_dir, | ||||
|         ): | ||||
|  | ||||
|             self.create_webp_thumbnail_files(self.thumbnail_dir, 3) | ||||
|  | ||||
|             self.performMigration() | ||||
|             run_convert_mock.assert_not_called() | ||||
|  | ||||
|             self.assert_webp_file_count(self.thumbnail_dir, 3) | ||||
|  | ||||
|     def test_convert_single_thumbnail( | ||||
|         self, | ||||
|         run_convert_mock: mock.MagicMock, | ||||
|         map_mock: mock.MagicMock, | ||||
|     ): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Document exists with PNG thumbnail | ||||
|         WHEN: | ||||
|             - Thumbnail conversion is attempted | ||||
|         THEN: | ||||
|             - Single thumbnail is converted | ||||
|         """ | ||||
|         map_mock.side_effect = self.pretend_map | ||||
|         run_convert_mock.side_effect = self.pretend_convert_output | ||||
|  | ||||
|         with override_settings( | ||||
|             THUMBNAIL_DIR=self.thumbnail_dir, | ||||
|         ): | ||||
|             self.create_png_thumbnail_file(self.thumbnail_dir, 3) | ||||
|  | ||||
|             self.performMigration() | ||||
|  | ||||
|             run_convert_mock.assert_called() | ||||
|             self.assertEqual(run_convert_mock.call_count, 3) | ||||
|  | ||||
|             self.assert_webp_file_count(self.thumbnail_dir, 3) | ||||
|  | ||||
|     def test_convert_errors_out( | ||||
|         self, | ||||
|         run_convert_mock: mock.MagicMock, | ||||
|         map_mock: mock.MagicMock, | ||||
|     ): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Document exists with PNG thumbnail | ||||
|         WHEN: | ||||
|             - Thumbnail conversion is attempted, but raises an exception | ||||
|         THEN: | ||||
|             - Single thumbnail is converted | ||||
|         """ | ||||
|         map_mock.side_effect = self.pretend_map | ||||
|         run_convert_mock.side_effect = OSError | ||||
|  | ||||
|         with override_settings( | ||||
|             THUMBNAIL_DIR=self.thumbnail_dir, | ||||
|         ): | ||||
|  | ||||
|             self.create_png_thumbnail_file(self.thumbnail_dir, 3) | ||||
|  | ||||
|             self.performMigration() | ||||
|  | ||||
|             run_convert_mock.assert_called() | ||||
|             self.assertEqual(run_convert_mock.call_count, 3) | ||||
|  | ||||
|             self.assert_png_file_count(self.thumbnail_dir, 3) | ||||
|  | ||||
|     def test_convert_mixed( | ||||
|         self, | ||||
|         run_convert_mock: mock.MagicMock, | ||||
|         map_mock: mock.MagicMock, | ||||
|     ): | ||||
|         """ | ||||
|         GIVEN: | ||||
|             - Document exists with PNG thumbnail | ||||
|         WHEN: | ||||
|             - Thumbnail conversion is attempted, but raises an exception | ||||
|         THEN: | ||||
|             - Single thumbnail is converted | ||||
|         """ | ||||
|         map_mock.side_effect = self.pretend_map | ||||
|         run_convert_mock.side_effect = self.pretend_convert_output | ||||
|  | ||||
|         with override_settings( | ||||
|             THUMBNAIL_DIR=self.thumbnail_dir, | ||||
|         ): | ||||
|  | ||||
|             self.create_png_thumbnail_file(self.thumbnail_dir, 3) | ||||
|             self.create_webp_thumbnail_files(self.thumbnail_dir, 2, start_count=3) | ||||
|  | ||||
|             self.performMigration() | ||||
|  | ||||
|             run_convert_mock.assert_called() | ||||
|             self.assertEqual(run_convert_mock.call_count, 3) | ||||
|  | ||||
|             self.assert_png_file_count(self.thumbnail_dir, 0) | ||||
|             self.assert_webp_file_count(self.thumbnail_dir, 5) | ||||
		Reference in New Issue
	
	Block a user
	 Trenton Holmes
					Trenton Holmes