metadata-scrubber-tool:

added end-to-end test for cli commands,
which includes for read and scrub commands.
added the version flag and modified the verbose to use capital V
This commit is contained in:
HERITAGE-XION 2026-01-06 23:45:36 +01:00
parent b628130596
commit ab70844c8e
6 changed files with 270 additions and 9 deletions

View File

@ -20,7 +20,7 @@ log = logging.getLogger("metadata-scrubber")
# fmt: off
def get_metadata(
def read(
file_path: Path = typer.Argument(
exists=True, # Must exist on the filesystem
file_okay=True, # Can be a file

View File

@ -13,7 +13,7 @@ import logging
import typer
from src.commands.read import get_metadata
from src.commands.read import read
from src.commands.scrub import scrub
from src.utils.logger import setup_logging
@ -22,18 +22,44 @@ app = typer.Typer(no_args_is_help=True, pretty_exceptions_show_locals=False)
log = logging.getLogger("metadata-scrubber")
__version__ = "0.1.0"
# ---------------------------------------------------------
# VERSION CALLBACK
# ---------------------------------------------------------
def version_callback(value: bool):
"""
Prints the version and exits.
"""
if value:
print(f"Version: {__version__}")
raise typer.Exit()
# ---------------------------------------------------------
# MAIN ENTRY POINT
# ---------------------------------------------------------
# fmt: off
@app.callback()
def main(
verbose: bool = typer.Option(
False,
"--verbose",
"-v",
"-V",
help="Show detailed debug logs for every file processed.",
),
version: bool = typer.Option(
None,
"--version",
"-v",
callback=version_callback,
is_eager=True,
help="Show the application version and exit."
),
):
"""
Metadata Scrubber Tool - Clean your images privacy data.
Metadata Scrubber Tool - Clean your images personal identifying data. eg: author name, camera model, GPS coordinates, etc.
"""
# Initialize the logger based on the user's flag
setup_logging(verbose)
@ -43,7 +69,7 @@ def main(
# fmt: on
# register commands
app.command(name="read")(get_metadata)
app.command(name="read")(read)
app.command(name="scrub")(scrub)
# run app

View File

@ -99,7 +99,7 @@ class BatchProcessor:
if self.dry_run:
# Verify the file can be handled (will raise if not)
MetadataFactory.get_handler(str(file))
output_path = self._get_unique_output_path(file)
output_path = self._get_unique_output_path(file, reserve=False)
result = FileResult(
filepath=file,
success=True,
@ -250,7 +250,7 @@ class BatchProcessor:
# Best effort cleanup - don't fail if we can't delete
pass
def _get_unique_output_path(self, file: Path) -> Path:
def _get_unique_output_path(self, file: Path, reserve: bool = True) -> Path:
"""
Generate unique output path with suffix (_1, _2) if file exists.
@ -258,6 +258,8 @@ class BatchProcessor:
Args:
file: Original file path.
reserve: If True, create placeholder file to reserve the path.
Set to False for dry-run mode.
Returns:
Unique path in output directory that doesn't conflict with existing files.
@ -278,7 +280,8 @@ class BatchProcessor:
)
counter += 1
# Create empty placeholder to reserve the path
output_path.touch()
# Create empty placeholder to reserve the path (skip in dry-run)
if reserve:
output_path.touch()
return output_path

24
tests/e2e/test_main.py Normal file
View File

@ -0,0 +1,24 @@
from typer.testing import CliRunner
from src.main import __version__, app
# Initialize the runner
runner = CliRunner()
def test_app_version():
"""
Ensure the --version flag prints the correct version and exits cleanly.
"""
result = runner.invoke(app, ["--version"])
assert result.exit_code == 0, "The app crashed!"
assert __version__ in result.stdout, "Version number not found in output"
def test_app_help():
"""
Ensure the --help flag works.
"""
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "Metadata Scrubber Tool" in result.stdout

View File

@ -0,0 +1,79 @@
from pathlib import Path
import pytest
from typer.testing import CliRunner
from src.main import app
runner = CliRunner()
# Test file paths
JPG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\DSCN0012.jpg"
PNG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\generated_test_45.png"
TEST_DIR = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images"
# ============== Success Case Tests ==============
@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 "Reading" in result.stdout
assert Path(x).name in result.stdout
@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 "Reading" in result.stdout
def test_read_command_requires_ext_with_recursive():
"""
Test that --recursive requires --extension flag.
"""
result = runner.invoke(app, ["read", TEST_DIR, "-r"])
assert result.exit_code != 0
# Should fail with bad parameter error
def test_read_command_requires_recursive_with_ext():
"""
Test that --extension requires --recursive flag.
"""
result = runner.invoke(app, ["read", JPG_TEST_FILE, "-ext", "jpg"])
assert result.exit_code != 0
# ============== Error Case Tests ==============
def test_read_command_file_not_found():
"""
Test that the app handles missing files gracefully.
"""
result = runner.invoke(app, ["read", "ghost_file.jpg"])
# Typer returns exit code 2 (Usage Error) for bad arguments
assert result.exit_code == 2
assert "Invalid value for 'FILE_PATH'" in result.stderr
assert "does not exist" in result.stderr

View File

@ -0,0 +1,129 @@
"""
E2E tests for the 'scrub' command.
Tests the full CLI flow for scrubbing metadata from files.
"""
from pathlib import Path
import pytest
from typer.testing import CliRunner
from src.main import app
runner = CliRunner()
# Test file paths - use examples folder for faster tests (fewer files)
JPG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\DSCN0012.jpg"
PNG_TEST_FILE = r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images\generated_test_45.png"
# Use examples folder (small) instead of full test_images folder (100+ files)
EXAMPLES_DIR = (
r"C:\Users\Xheri\development\metadata-scrubber-tool\tests\assets\test_images"
)
@pytest.fixture
def output_dir(tmp_path):
"""Create isolated output directory for each test using tmp_path."""
output = tmp_path / "output"
output.mkdir(parents=True, exist_ok=True)
return output
# ============== Success Case Tests ==============
@pytest.mark.parametrize("x", [JPG_TEST_FILE, PNG_TEST_FILE])
def test_scrub_command_single_file_success(x, output_dir):
"""
Test the 'scrub' command with a single file.
"""
result = runner.invoke(app, ["scrub", x, "--output", str(output_dir)])
assert result.exit_code == 0
# Check output file was created (has processed_ prefix)
output_file = output_dir / f"processed_{Path(x).name}"
assert output_file.exists()
def test_scrub_command_recursive_jpg_success(output_dir):
"""
Test the 'scrub' command with recursive directory processing for JPG.
Uses examples folder (smaller) for faster tests.
"""
result = runner.invoke(
app, ["scrub", EXAMPLES_DIR, "-r", "-ext", "jpg", "--output", str(output_dir)]
)
assert result.exit_code == 0
# Check at least one output file was created
output_files = list(output_dir.glob("processed_*.jpg"))
assert len(output_files) > 0
def test_scrub_command_dry_run(output_dir):
"""
Test that --dry-run doesn't create files.
"""
result = runner.invoke(
app, ["scrub", JPG_TEST_FILE, "--output", str(output_dir), "--dry-run"]
)
assert result.exit_code == 0
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}"
assert not output_file.exists()
def test_scrub_command_with_workers(output_dir):
"""
Test the --workers option for concurrent processing.
"""
result = runner.invoke(
app,
[
"scrub",
EXAMPLES_DIR,
"-r",
"-ext",
"jpg",
"--output",
str(output_dir),
"--workers",
"2",
],
)
assert result.exit_code == 0
# ============== Error Case Tests ==============
def test_scrub_command_file_not_found():
"""
Test that the app handles missing files gracefully.
"""
result = runner.invoke(app, ["scrub", "ghost_file.jpg"])
assert result.exit_code == 2
assert "Invalid value" in result.stderr
def test_scrub_command_requires_ext_with_recursive():
"""
Test that --recursive requires --extension flag.
"""
result = runner.invoke(app, ["scrub", EXAMPLES_DIR, "-r"])
assert result.exit_code != 0
def test_scrub_command_requires_recursive_with_ext():
"""
Test that --extension requires --recursive flag.
"""
result = runner.invoke(app, ["scrub", JPG_TEST_FILE, "-ext", "jpg"])
assert result.exit_code != 0