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:
parent
b628130596
commit
ab70844c8e
|
|
@ -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
|
||||
|
|
|
|||
34
src/main.py
34
src/main.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue