3b9d1ef1a8
- Replace the Docker build/publish CI stages with ruff (lint + format check), ty (type check), pip-audit, and pytest run directly against python:3.11-slim; Docker remains for manual/server deployment only. - Swap black/pylint/mypy for ruff/ty across pyproject.toml, and fix every resulting lint, format, and type diagnostic in gallery/ and plotstyle/. - Fix tests broken/stale from before the package restructuring: wrong `utils.*` import paths, mock patch targets pointed at the wrong module, and PDF-conversion tests still assuming ImageMagick instead of the current PyMuPDF-first path. Drop test_container.py (obsolete Docker-container smoke tests, fully superseded elsewhere). - Add a plain-venv + systemd --user timer deployment path (deploy/systemd/) for HPC login nodes without a Docker daemon, where public_html is already served by existing infrastructure. - Document both in CLAUDE.md, including running CI's checks locally before committing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
280 lines
7.8 KiB
Python
280 lines
7.8 KiB
Python
import os
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from gallery import (
|
|
build_gallery,
|
|
calculate_directory_stats,
|
|
convert_pdf_to_png,
|
|
datetime_from_timestamp,
|
|
format_file_size,
|
|
needs_update,
|
|
strftime_filter,
|
|
)
|
|
|
|
|
|
def test_format_file_size():
|
|
assert format_file_size(0) == "0 B"
|
|
assert format_file_size(1) == "1.0 B"
|
|
assert format_file_size(1024) == "1.0 KB"
|
|
assert format_file_size(1048576) == "1.0 MB"
|
|
assert format_file_size(1073741824) == "1.0 GB"
|
|
assert format_file_size(1099511627776) == "1.0 TB"
|
|
|
|
|
|
def test_format_file_size_edge_cases():
|
|
assert format_file_size(1023) == "1023.0 B"
|
|
assert format_file_size(1536) == "1.5 KB"
|
|
assert format_file_size(2621440) == "2.5 MB"
|
|
|
|
|
|
def test_needs_update_missing_target(tmp_path):
|
|
source = tmp_path / "source.txt"
|
|
target = tmp_path / "target.txt"
|
|
source.write_text("test content")
|
|
|
|
assert needs_update(source, target) is True
|
|
|
|
|
|
def test_needs_update_target_newer(tmp_path):
|
|
source = tmp_path / "source.txt"
|
|
target = tmp_path / "target.txt"
|
|
|
|
source.write_text("test")
|
|
target.write_text("test")
|
|
|
|
# Make target newer by modifying its timestamp
|
|
import time
|
|
|
|
time.sleep(0.1)
|
|
target.touch()
|
|
|
|
assert needs_update(source, target) is False
|
|
|
|
|
|
def test_needs_update_source_newer(tmp_path):
|
|
source = tmp_path / "source.txt"
|
|
target = tmp_path / "target.txt"
|
|
|
|
target.write_text("test")
|
|
import time
|
|
|
|
time.sleep(0.1)
|
|
source.write_text("test")
|
|
|
|
# Force different modification times with 31+ second buffer
|
|
target_time = target.stat().st_mtime
|
|
source_time = target_time + 40 # 40 seconds newer (> 30 second buffer)
|
|
os.utime(source, (source_time, source_time))
|
|
|
|
assert needs_update(source, target) is True
|
|
|
|
|
|
@patch("gallery.utils.processing._convert_pdf_pymupdf")
|
|
def test_convert_pdf_to_png_success(mock_convert, tmp_path):
|
|
from gallery import GalleryConfig
|
|
|
|
pdf_path = tmp_path / "test.pdf"
|
|
png_path = tmp_path / "test.png"
|
|
pdf_path.write_text("fake pdf")
|
|
|
|
config = GalleryConfig(tmp_path)
|
|
convert_pdf_to_png(pdf_path, config)
|
|
|
|
mock_convert.assert_called_once_with(pdf_path, png_path, config.png_dpi)
|
|
|
|
|
|
@patch("gallery.utils.processing._convert_pdf_pymupdf")
|
|
def test_convert_pdf_to_png_already_exists_newer(mock_convert, tmp_path):
|
|
from gallery import GalleryConfig
|
|
|
|
pdf_path = tmp_path / "test.pdf"
|
|
png_path = tmp_path / "test.png"
|
|
|
|
pdf_path.write_text("fake pdf")
|
|
png_path.write_text("fake png")
|
|
|
|
# Make PNG much newer than PDF using explicit time
|
|
pdf_time = pdf_path.stat().st_mtime
|
|
png_time = pdf_time + 100 # PNG is 100 seconds newer
|
|
os.utime(png_path, (png_time, png_time))
|
|
|
|
convert_pdf_to_png(pdf_path, GalleryConfig(tmp_path))
|
|
|
|
# Should not convert since PNG is newer
|
|
mock_convert.assert_not_called()
|
|
|
|
|
|
@patch("gallery.utils.processing._convert_pdf_pymupdf")
|
|
def test_convert_pdf_to_png_pdf_newer(mock_convert, tmp_path):
|
|
from gallery import GalleryConfig
|
|
|
|
pdf_path = tmp_path / "test.pdf"
|
|
png_path = tmp_path / "test.png"
|
|
|
|
png_path.write_text("fake png")
|
|
import time
|
|
|
|
time.sleep(0.1)
|
|
pdf_path.write_text("fake pdf")
|
|
|
|
convert_pdf_to_png(pdf_path, GalleryConfig(tmp_path))
|
|
|
|
# Should convert since PDF is newer
|
|
mock_convert.assert_called_once()
|
|
|
|
|
|
def test_calculate_directory_stats_empty(tmp_path):
|
|
stats = calculate_directory_stats(tmp_path)
|
|
|
|
assert stats["file_count"] == 0
|
|
assert stats["folder_count"] == 0
|
|
assert stats["total_size"] == 0
|
|
assert stats["pdf_size"] == 0
|
|
assert stats["png_size"] == 0
|
|
|
|
|
|
def test_calculate_directory_stats_with_files(tmp_path):
|
|
# Create test files
|
|
(tmp_path / "test.pdf").write_text("pdf content")
|
|
(tmp_path / "test.png").write_text("png content")
|
|
(tmp_path / "test.txt").write_text("txt content")
|
|
|
|
# Create subdirectory
|
|
subdir = tmp_path / "subdir"
|
|
subdir.mkdir()
|
|
(subdir / "nested.pdf").write_text("nested pdf")
|
|
|
|
stats = calculate_directory_stats(tmp_path)
|
|
|
|
assert stats["file_count"] == 4
|
|
assert stats["folder_count"] == 1
|
|
assert stats["total_size"] > 0
|
|
assert stats["pdf_size"] > 0
|
|
assert stats["png_size"] > 0
|
|
|
|
|
|
def test_calculate_directory_stats_nonexistent():
|
|
nonexistent = Path("/nonexistent/path")
|
|
stats = calculate_directory_stats(nonexistent)
|
|
|
|
assert stats["file_count"] == 0
|
|
assert stats["folder_count"] == 0
|
|
assert stats["total_size"] == 0
|
|
|
|
|
|
def test_datetime_from_timestamp():
|
|
timestamp = 1630000000 # Some Unix timestamp
|
|
dt = datetime_from_timestamp(timestamp)
|
|
|
|
assert isinstance(dt, datetime)
|
|
assert dt.timestamp() == timestamp
|
|
|
|
|
|
def test_strftime_filter():
|
|
dt = datetime(2023, 9, 8, 14, 30, 0)
|
|
formatted = strftime_filter(dt, "%Y-%m-%d %H:%M")
|
|
|
|
assert formatted == "2023-09-08 14:30"
|
|
|
|
|
|
@patch("gallery.builder.save_metadata_cache")
|
|
@patch("gallery.utils.processing.resolve_metadata_for_plot")
|
|
@patch("gallery.builder.merge_metadata")
|
|
@patch("gallery.builder.load_folder_metadata")
|
|
@patch("gallery.utils.processing.convert_pdf_to_png")
|
|
@patch("shutil.copy2")
|
|
def test_build_gallery_basic(
|
|
mock_copy, mock_convert, mock_load_folder, mock_merge, mock_resolve, mock_save_cache, tmp_path
|
|
):
|
|
from gallery import GalleryConfig, get_template
|
|
|
|
source_dir = tmp_path / "source"
|
|
web_dir = tmp_path / "web"
|
|
source_dir.mkdir()
|
|
web_dir.mkdir()
|
|
|
|
# Create a test PDF
|
|
pdf_file = source_dir / "test.pdf"
|
|
pdf_file.write_text("fake pdf content")
|
|
|
|
# Don't create PNG - this will trigger convert_pdf_to_png call
|
|
|
|
# Mock returns
|
|
mock_load_folder.return_value = {"folder": "metadata"}
|
|
mock_merge.return_value = {"merged": "metadata"}
|
|
mock_resolve.return_value = {"plot": "metadata"}
|
|
|
|
config = GalleryConfig(tmp_path)
|
|
build_gallery(config, source_dir, web_dir, template=get_template())
|
|
|
|
# Verify mocks were called
|
|
mock_convert.assert_called_once_with(pdf_file, config=config)
|
|
mock_copy.assert_called() # Should be called for PDF
|
|
mock_save_cache.assert_called_once()
|
|
|
|
# Check HTML file was created by the real render_gallery_page
|
|
html_file = web_dir / "index.html"
|
|
assert html_file.exists()
|
|
|
|
|
|
@patch("gallery.builder.render_gallery_page")
|
|
@patch("gallery.builder.save_metadata_cache")
|
|
@patch("gallery.builder.load_folder_metadata")
|
|
def test_build_gallery_with_subdirs(mock_load_folder, mock_save_cache, mock_template, tmp_path):
|
|
source_dir = tmp_path / "source"
|
|
web_dir = tmp_path / "web"
|
|
source_dir.mkdir()
|
|
web_dir.mkdir()
|
|
|
|
# Create subdirectory
|
|
subdir = source_dir / "subdir"
|
|
subdir.mkdir()
|
|
|
|
mock_load_folder.return_value = {}
|
|
mock_template.render.return_value = "<html>test</html>"
|
|
|
|
from gallery import GalleryConfig
|
|
|
|
build_gallery(config=GalleryConfig(tmp_path), source_dir=source_dir, web_dir=web_dir)
|
|
|
|
# Check subdirectory was created in web
|
|
web_subdir = web_dir / "subdir"
|
|
assert web_subdir.exists()
|
|
assert web_subdir.is_dir()
|
|
|
|
|
|
@patch("gallery.utils.processing.needs_update")
|
|
@patch("shutil.copy2")
|
|
def test_build_gallery_skip_up_to_date(mock_copy, mock_needs_update, tmp_path):
|
|
source_dir = tmp_path / "source"
|
|
web_dir = tmp_path / "web"
|
|
source_dir.mkdir()
|
|
web_dir.mkdir()
|
|
|
|
# Create test files
|
|
pdf_file = source_dir / "test.pdf"
|
|
png_file = source_dir / "test.png"
|
|
pdf_file.write_text("pdf")
|
|
png_file.write_text("png")
|
|
|
|
# Create target files
|
|
web_pdf = web_dir / "test.pdf"
|
|
web_png = web_dir / "test.png"
|
|
web_pdf.write_text("pdf")
|
|
web_png.write_text("png")
|
|
|
|
# Mock needs_update to return False (up to date)
|
|
mock_needs_update.return_value = False
|
|
|
|
from gallery import get_template
|
|
|
|
get_template()
|
|
from gallery import GalleryConfig
|
|
|
|
build_gallery(GalleryConfig(tmp_path), source_dir, web_dir)
|
|
|
|
# copy2 should not be called since files are up to date
|
|
mock_copy.assert_not_called()
|