diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..f882d808 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Tests + +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] # Test multiple versions + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --all-extras --dev + + - name: Run Tests + run: uv run pytest -x \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..f2167b54 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,48 @@ +""" +Pytest configuration and shared fixtures. + +Provides cross-platform test file paths that work on both Windows and Linux (CI). +""" + +from pathlib import Path + +import pytest + +# Get the tests directory (this file's parent) +TESTS_DIR = Path(__file__).parent +ASSETS_DIR = TESTS_DIR / "assets" +TEST_IMAGES_DIR = ASSETS_DIR / "test_images" + + +@pytest.fixture +def jpg_test_file() -> Path: + """Return path to a JPG test file.""" + return TEST_IMAGES_DIR / "test_fuji.jpg" + + +@pytest.fixture +def png_test_file() -> Path: + """Return path to a PNG test file.""" + return TEST_IMAGES_DIR / "generated_test_03.png" + + +@pytest.fixture +def test_images_dir() -> Path: + """Return path to test images directory.""" + return TEST_IMAGES_DIR + + +# String versions for parametrize (which doesn't support fixtures directly) +def get_jpg_test_file() -> str: + """Get JPG test file path as string.""" + return str(TEST_IMAGES_DIR / "test_fuji.jpg") + + +def get_png_test_file() -> str: + """Get PNG test file path as string.""" + return str(TEST_IMAGES_DIR / "generated_test_03.png") + + +def get_test_images_dir() -> str: + """Get test images directory path as string.""" + return str(TEST_IMAGES_DIR) diff --git a/tests/e2e/test_read_command.py b/tests/e2e/test_read_command.py index 840461b4..9972a77c 100644 --- a/tests/e2e/test_read_command.py +++ b/tests/e2e/test_read_command.py @@ -5,43 +5,40 @@ from typer.testing import CliRunner from src.main import app +# Import path helpers from conftest +from tests.conftest import get_jpg_test_file, get_png_test_file, get_test_images_dir + runner = CliRunner() -# Test file paths -JPG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\test_fuji.jpg" -PNG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\generated_test_03.png" -TEST_DIR = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images" +# Test file paths (cross-platform) +JPG_TEST_FILE = get_jpg_test_file() +PNG_TEST_FILE = get_png_test_file() +TEST_DIR = get_test_images_dir() # ============== Success Case Tests ============== -@pytest.mark.parametrize( - "x", - [JPG_TEST_FILE, PNG_TEST_FILE], -) +@pytest.mark.parametrize("x", [JPG_TEST_FILE, PNG_TEST_FILE]) def test_read_command_single_file_success(x): """ Test the 'read' command with a single file (JPG and PNG). """ result = runner.invoke(app, ["read", str(x)]) - assert result.exit_code == 0 + assert result.exit_code == 0, f"Failed with: {result.stdout}" assert "Reading" in result.stdout assert Path(x).name in result.stdout -@pytest.mark.parametrize( - "ext", - ["jpg", "png"], -) +@pytest.mark.parametrize("ext", ["jpg", "png"]) def test_read_command_recursive_directory_success(ext): """ Test the 'read' command with recursive directory processing. """ result = runner.invoke(app, ["read", TEST_DIR, "-r", "-ext", ext]) - assert result.exit_code == 0 + assert result.exit_code == 0, f"Failed with: {result.stdout}" assert "Reading" in result.stdout diff --git a/tests/e2e/test_scrub_command.py b/tests/e2e/test_scrub_command.py index 21bcc28a..bc64e482 100644 --- a/tests/e2e/test_scrub_command.py +++ b/tests/e2e/test_scrub_command.py @@ -11,14 +11,15 @@ from typer.testing import CliRunner from src.main import app +# Import path helpers from conftest +from tests.conftest import get_jpg_test_file, get_png_test_file, get_test_images_dir + runner = CliRunner() -# Test file paths -JPG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\test_fuji.jpg" -PNG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\generated_test_03.png" -EXAMPLES_DIR = ( - r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images" -) +# Test file paths (cross-platform) +JPG_TEST_FILE = get_jpg_test_file() +PNG_TEST_FILE = get_png_test_file() +EXAMPLES_DIR = get_test_images_dir() @pytest.fixture @@ -39,7 +40,7 @@ def test_scrub_command_single_file_success(x, output_dir): """ result = runner.invoke(app, ["scrub", x, "--output", str(output_dir)]) - assert result.exit_code == 0 + assert result.exit_code == 0, f"Failed with: {result.stdout}" # Check output file was created (has processed_ prefix) output_file = output_dir / f"processed_{Path(x).name}" assert output_file.exists() @@ -54,7 +55,7 @@ def test_scrub_command_recursive_jpg_success(output_dir): app, ["scrub", EXAMPLES_DIR, "-r", "-ext", "jpg", "--output", str(output_dir)] ) - assert result.exit_code == 0 + assert result.exit_code == 0, f"Failed with: {result.stdout}" # Check at least one output file was created output_files = list(output_dir.glob("processed_*.jpg")) assert len(output_files) > 0 @@ -68,7 +69,7 @@ def test_scrub_command_dry_run(output_dir): app, ["scrub", JPG_TEST_FILE, "--output", str(output_dir), "--dry-run"] ) - assert result.exit_code == 0 + assert result.exit_code == 0, f"Failed with: {result.stdout}" assert "DRY-RUN" in result.stdout # No files should be created in dry-run mode output_file = output_dir / f"processed_{Path(JPG_TEST_FILE).name}" @@ -94,7 +95,7 @@ def test_scrub_command_with_workers(output_dir): ], ) - assert result.exit_code == 0 + assert result.exit_code == 0, f"Failed with: {result.stdout}" # ============== Error Case Tests ============== diff --git a/tests/integration/test_metadata_factory.py b/tests/integration/test_metadata_factory.py index db9656b6..6136bcd2 100644 --- a/tests/integration/test_metadata_factory.py +++ b/tests/integration/test_metadata_factory.py @@ -4,25 +4,26 @@ from pathlib import Path import pytest from src.services.metadata_factory import MetadataFactory -from src.utils.exceptions import MetadataNotFoundError +from src.utils.exceptions import MetadataNotFoundError, UnsupportedFormatError -# Test file paths -JPG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\test_fuji.jpg" -PNG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\generated_test_03.png" +# Import path helpers from conftest +from tests.conftest import get_jpg_test_file, get_png_test_file -# ============== success Case Tests ============== +# Test file paths (cross-platform) +JPG_TEST_FILE = get_jpg_test_file() +PNG_TEST_FILE = get_png_test_file() -@pytest.mark.parametrize( - "x", - [JPG_TEST_FILE, PNG_TEST_FILE], -) +# ============== Success Case Tests ============== + + +@pytest.mark.parametrize("x", [JPG_TEST_FILE, PNG_TEST_FILE]) def test_read_image_metadata(x): """ test for reading image metadata """ # checks if file exists - assert Path(x).exists() + assert Path(x).exists(), f"Test file not found: {x}" handler = MetadataFactory.get_handler(str(x)) metadata = handler.read() @@ -36,16 +37,13 @@ def test_read_image_metadata(x): assert isinstance(metadata, dict) -@pytest.mark.parametrize( - "x", - [JPG_TEST_FILE, PNG_TEST_FILE], -) +@pytest.mark.parametrize("x", [JPG_TEST_FILE, PNG_TEST_FILE]) def test_wipe_image_metadata(x): """ test for wiping image metadata """ # checks if file exists - assert Path(x).exists() + assert Path(x).exists(), f"Test file not found: {x}" handler = MetadataFactory.get_handler(str(x)) metadata = handler.read() handler.wipe() @@ -54,10 +52,7 @@ def test_wipe_image_metadata(x): assert handler.processed_metadata != metadata -@pytest.mark.parametrize( - "x", - [JPG_TEST_FILE, PNG_TEST_FILE], -) +@pytest.mark.parametrize("x", [JPG_TEST_FILE, PNG_TEST_FILE]) def test_save_processed_image_metadata(x): """ test for saving image processed metadata to a copy of the file @@ -82,10 +77,7 @@ def test_save_processed_image_metadata(x): shutil.rmtree(output_dir) -@pytest.mark.parametrize( - "x", - [JPG_TEST_FILE, PNG_TEST_FILE], -) +@pytest.mark.parametrize("x", [JPG_TEST_FILE, PNG_TEST_FILE]) def test_output_file_has_less_metadata(x): """ Test that the output file has metadata stripped @@ -136,8 +128,6 @@ def test_unsupported_format_raises_error(tmp_path): """ Test that unsupported file formats raise an error """ - from src.utils.exceptions import UnsupportedFormatError - # Create a fake text file fake_file = tmp_path / "test.txt" fake_file.write_text("not an image") diff --git a/tests/unit/test_image_handler.py b/tests/unit/test_image_handler.py index 4bf31419..742c02a3 100644 --- a/tests/unit/test_image_handler.py +++ b/tests/unit/test_image_handler.py @@ -6,24 +6,24 @@ import pytest from src.services.image_handler import ImageHandler from src.utils.exceptions import MetadataNotFoundError, UnsupportedFormatError -# Test file paths -JPG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\test_fuji.jpg" -PNG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\generated_test_03.png" +# Import path helpers from conftest +from tests.conftest import get_jpg_test_file, get_png_test_file + +# Test file paths (cross-platform) +JPG_TEST_FILE = get_jpg_test_file() +PNG_TEST_FILE = get_png_test_file() -# ============== success Case Tests ============== +# ============== Success Case Tests ============== -@pytest.mark.parametrize( - "x", - [JPG_TEST_FILE, PNG_TEST_FILE], -) +@pytest.mark.parametrize("x", [JPG_TEST_FILE, PNG_TEST_FILE]) def test_read_image_metadata(x): """ test for reading image metadata """ # checks if file exists - assert Path(x).exists() + assert Path(x).exists(), f"Test file not found: {x}" processor = ImageHandler(x) metadata = processor.read() @@ -40,16 +40,13 @@ def test_read_image_metadata(x): assert isinstance(metadata, dict) -@pytest.mark.parametrize( - "x", - [JPG_TEST_FILE, PNG_TEST_FILE], -) +@pytest.mark.parametrize("x", [JPG_TEST_FILE, PNG_TEST_FILE]) def test_wipe_image_metadata(x): """ test for wiping image metadata """ # checks if file exists - assert Path(x).exists() + assert Path(x).exists(), f"Test file not found: {x}" processor = ImageHandler(x) metadata = processor.read() processor.wipe() @@ -58,10 +55,7 @@ def test_wipe_image_metadata(x): assert processor.processed_metadata != metadata -@pytest.mark.parametrize( - "x", - [JPG_TEST_FILE, PNG_TEST_FILE], -) +@pytest.mark.parametrize("x", [JPG_TEST_FILE, PNG_TEST_FILE]) def test_save_processed_image_metadata(x): """ test for saving image processed metadata to a copy of the file @@ -86,10 +80,37 @@ def test_save_processed_image_metadata(x): shutil.rmtree(output_dir) -@pytest.mark.parametrize( - "x", - [JPG_TEST_FILE, PNG_TEST_FILE], -) +# ============== Error Case Tests ============== + + +def test_unsupported_format_raises_error(tmp_path): + """ + Test that unsupported file formats raise an error (PIL or custom) + """ + from PIL import UnidentifiedImageError + + # Create a fake text file + fake_file = tmp_path / "test.txt" + fake_file.write_text("not an image") + + handler = ImageHandler(str(fake_file)) + # Either our UnsupportedFormatError or PIL's UnidentifiedImageError is acceptable + with pytest.raises((UnsupportedFormatError, UnidentifiedImageError)): + handler.read() + + +def test_save_without_output_path_raises_error(): + """ + Test that save() raises ValueError when output_path is None + """ + handler = ImageHandler(JPG_TEST_FILE) + handler.read() + handler.wipe() + with pytest.raises(ValueError): + handler.save(None) + + +@pytest.mark.parametrize("x", [JPG_TEST_FILE, PNG_TEST_FILE]) def test_output_file_has_less_metadata(x): """ Test that the output file has metadata stripped @@ -131,33 +152,3 @@ def test_format_detection_works(): handler_png = ImageHandler(PNG_TEST_FILE) detected_png = handler_png._detect_format() assert detected_png == "png" - - -# ============== Error Case Tests ============== - - -def test_unsupported_format_raises_error(tmp_path): - """ - Test that unsupported file formats raise an error (PIL or custom) - """ - from PIL import UnidentifiedImageError - - # Create a fake text file - fake_file = tmp_path / "test.txt" - fake_file.write_text("not an image") - - handler = ImageHandler(str(fake_file)) - # Either our UnsupportedFormatError or PIL's UnidentifiedImageError is acceptable - with pytest.raises((UnsupportedFormatError, UnidentifiedImageError)): - handler.read() - - -def test_save_without_output_path_raises_error(): - """ - Test that save() raises ValueError when output_path is None - """ - handler = ImageHandler(JPG_TEST_FILE) - handler.read() - handler.wipe() - with pytest.raises(ValueError): - handler.save(None)