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
|
# fmt: off
|
||||||
def get_metadata(
|
def read(
|
||||||
file_path: Path = typer.Argument(
|
file_path: Path = typer.Argument(
|
||||||
exists=True, # Must exist on the filesystem
|
exists=True, # Must exist on the filesystem
|
||||||
file_okay=True, # Can be a file
|
file_okay=True, # Can be a file
|
||||||
|
|
|
||||||
34
src/main.py
34
src/main.py
|
|
@ -13,7 +13,7 @@ import logging
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
from src.commands.read import get_metadata
|
from src.commands.read import read
|
||||||
from src.commands.scrub import scrub
|
from src.commands.scrub import scrub
|
||||||
from src.utils.logger import setup_logging
|
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")
|
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
|
# fmt: off
|
||||||
@app.callback()
|
@app.callback()
|
||||||
def main(
|
def main(
|
||||||
verbose: bool = typer.Option(
|
verbose: bool = typer.Option(
|
||||||
False,
|
False,
|
||||||
"--verbose",
|
"--verbose",
|
||||||
"-v",
|
"-V",
|
||||||
help="Show detailed debug logs for every file processed.",
|
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
|
# Initialize the logger based on the user's flag
|
||||||
setup_logging(verbose)
|
setup_logging(verbose)
|
||||||
|
|
@ -43,7 +69,7 @@ def main(
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
# register commands
|
# register commands
|
||||||
app.command(name="read")(get_metadata)
|
app.command(name="read")(read)
|
||||||
app.command(name="scrub")(scrub)
|
app.command(name="scrub")(scrub)
|
||||||
|
|
||||||
# run app
|
# run app
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ class BatchProcessor:
|
||||||
if self.dry_run:
|
if self.dry_run:
|
||||||
# Verify the file can be handled (will raise if not)
|
# Verify the file can be handled (will raise if not)
|
||||||
MetadataFactory.get_handler(str(file))
|
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(
|
result = FileResult(
|
||||||
filepath=file,
|
filepath=file,
|
||||||
success=True,
|
success=True,
|
||||||
|
|
@ -250,7 +250,7 @@ class BatchProcessor:
|
||||||
# Best effort cleanup - don't fail if we can't delete
|
# Best effort cleanup - don't fail if we can't delete
|
||||||
pass
|
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.
|
Generate unique output path with suffix (_1, _2) if file exists.
|
||||||
|
|
||||||
|
|
@ -258,6 +258,8 @@ class BatchProcessor:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file: Original file path.
|
file: Original file path.
|
||||||
|
reserve: If True, create placeholder file to reserve the path.
|
||||||
|
Set to False for dry-run mode.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Unique path in output directory that doesn't conflict with existing files.
|
Unique path in output directory that doesn't conflict with existing files.
|
||||||
|
|
@ -278,7 +280,8 @@ class BatchProcessor:
|
||||||
)
|
)
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
# Create empty placeholder to reserve the path
|
# Create empty placeholder to reserve the path (skip in dry-run)
|
||||||
output_path.touch()
|
if reserve:
|
||||||
|
output_path.touch()
|
||||||
|
|
||||||
return output_path
|
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