Merge main into dev: bring dev up to date with restructured package

Resolves add/add conflicts in .gitignore, config.yaml, pyproject.toml and
content conflict in README.md — taking main's version in all cases, as it
reflects the complete package rewrite (gallery/ package, TUI, CI pipeline).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 11:45:45 +02:00
74 changed files with 11213 additions and 150 deletions
+4
View File
@@ -0,0 +1,4 @@
import sys
sys.path.append("..")
+214
View File
@@ -0,0 +1,214 @@
import zipfile
import datetime
from unittest.mock import patch
from utils import backup
def test_backup_creates_zip(tmp_path, monkeypatch):
# Setup fake web folder
web_folder = tmp_path / 'plots'
web_folder.mkdir()
(web_folder / 'file1.txt').write_text('abc')
(web_folder / 'file2.txt').write_text('def')
backup_folder = tmp_path / 'backups'
backup_folder.mkdir()
# Patch the module variables
monkeypatch.setattr(backup, 'WEB_FOLDER', web_folder)
monkeypatch.setattr(backup, 'BACKUP_FOLDER', backup_folder)
# Call the backup function
backup.create_backup()
# Check that backup was created
today = datetime.date.today().strftime('%Y%m%d')
backup_name = f'backup-{today}.zip'
backup_path = backup_folder / backup_name
assert backup_path.exists()
with zipfile.ZipFile(backup_path, 'r') as z:
names = z.namelist()
assert any('file1.txt' in n for n in names)
assert any('file2.txt' in n for n in names)
# Cleanup: remove the backup file after test
backup_path.unlink()
def test_backup_with_subdirectories(tmp_path, monkeypatch):
# Setup fake web folder with subdirectories
web_folder = tmp_path / 'plots'
web_folder.mkdir()
(web_folder / 'file1.txt').write_text('content1')
subdir = web_folder / 'subdir'
subdir.mkdir()
(subdir / 'file2.txt').write_text('content2')
nested_subdir = subdir / 'nested'
nested_subdir.mkdir()
(nested_subdir / 'file3.txt').write_text('content3')
backup_folder = tmp_path / 'backups'
backup_folder.mkdir()
# Patch the module variables
monkeypatch.setattr(backup, 'WEB_FOLDER', web_folder)
monkeypatch.setattr(backup, 'BACKUP_FOLDER', backup_folder)
# Call the backup function
backup.create_backup()
# Check that backup was created with all files
today = datetime.date.today().strftime('%Y%m%d')
backup_name = f'backup-{today}.zip'
backup_path = backup_folder / backup_name
assert backup_path.exists()
with zipfile.ZipFile(backup_path, 'r') as z:
names = z.namelist()
assert any('file1.txt' in n for n in names)
assert any('file2.txt' in n for n in names)
assert any('file3.txt' in n for n in names)
# Cleanup
backup_path.unlink()
def test_backup_existing_file(tmp_path, monkeypatch, capsys):
# Setup fake web folder
web_folder = tmp_path / 'plots'
web_folder.mkdir()
(web_folder / 'file1.txt').write_text('abc')
backup_folder = tmp_path / 'backups'
backup_folder.mkdir()
# Create existing backup file
today = datetime.date.today().strftime('%Y%m%d')
backup_name = f'backup-{today}.zip'
backup_path = backup_folder / backup_name
backup_path.write_text('existing backup')
# Patch the module variables
monkeypatch.setattr(backup, 'WEB_FOLDER', web_folder)
monkeypatch.setattr(backup, 'BACKUP_FOLDER', backup_folder)
# Call the backup function
backup.create_backup()
# Check that message about existing backup was printed
captured = capsys.readouterr()
assert f"Backup already exists: {backup_path}" in captured.out
# Cleanup
backup_path.unlink()
def test_backup_empty_folder(tmp_path, monkeypatch):
# Setup empty web folder
web_folder = tmp_path / 'plots'
web_folder.mkdir()
backup_folder = tmp_path / 'backups'
backup_folder.mkdir()
# Patch the module variables
monkeypatch.setattr(backup, 'WEB_FOLDER', web_folder)
monkeypatch.setattr(backup, 'BACKUP_FOLDER', backup_folder)
# Call the backup function
backup.create_backup()
# Check that backup was created (empty)
today = datetime.date.today().strftime('%Y%m%d')
backup_name = f'backup-{today}.zip'
backup_path = backup_folder / backup_name
assert backup_path.exists()
with zipfile.ZipFile(backup_path, 'r') as z:
assert len(z.namelist()) == 0
# Cleanup
backup_path.unlink()
def test_backup_nonexistent_web_folder(tmp_path, monkeypatch):
# Setup nonexistent web folder
web_folder = tmp_path / 'nonexistent_plots'
backup_folder = tmp_path / 'backups'
backup_folder.mkdir()
# Patch the module variables
monkeypatch.setattr(backup, 'WEB_FOLDER', web_folder)
monkeypatch.setattr(backup, 'BACKUP_FOLDER', backup_folder)
# Call the backup function
backup.create_backup()
# Check that backup was created (empty since source doesn't exist)
today = datetime.date.today().strftime('%Y%m%d')
backup_name = f'backup-{today}.zip'
backup_path = backup_folder / backup_name
assert backup_path.exists()
with zipfile.ZipFile(backup_path, 'r') as z:
assert len(z.namelist()) == 0
# Cleanup
backup_path.unlink()
@patch('datetime.date')
def test_backup_with_custom_date(mock_date, tmp_path, monkeypatch):
# Mock date to return a specific date
mock_date.today.return_value.strftime.return_value = "20230908"
# Setup fake web folder
web_folder = tmp_path / 'plots'
web_folder.mkdir()
(web_folder / 'file1.txt').write_text('test')
backup_folder = tmp_path / 'backups'
backup_folder.mkdir()
# Patch the module variables
monkeypatch.setattr(backup, 'WEB_FOLDER', web_folder)
monkeypatch.setattr(backup, 'BACKUP_FOLDER', backup_folder)
# Call the backup function
backup.create_backup()
# Check that backup was created with custom date
backup_path = backup_folder / "backup-20230908.zip"
assert backup_path.exists()
# Cleanup
backup_path.unlink()
def test_backup_folder_creation(tmp_path, monkeypatch):
# Setup fake web folder
web_folder = tmp_path / 'plots'
web_folder.mkdir()
(web_folder / 'file1.txt').write_text('test')
# Don't create backup folder - let function create it
backup_folder = tmp_path / 'new_backups'
# Patch the module variables
monkeypatch.setattr(backup, 'WEB_FOLDER', web_folder)
monkeypatch.setattr(backup, 'BACKUP_FOLDER', backup_folder)
# Call the backup function
backup.create_backup()
# Check that backup folder was created
assert backup_folder.exists()
assert backup_folder.is_dir()
# Check that backup file was created
today = datetime.date.today().strftime('%Y%m%d')
backup_name = f'backup-{today}.zip'
backup_path = backup_folder / backup_name
assert backup_path.exists()
+195
View File
@@ -0,0 +1,195 @@
from pathlib import Path
import tempfile
import pytest
import yaml
from utils import config
def test_path_config():
pc = config.PathConfig(work_dir='/tmp', web_folder='/web')
assert pc.work_dir == '/tmp'
assert pc.web_folder == '/web'
def test_gallery_config():
gc = config.GalleryConfig(
plot_root='plots', png_dpi=150, backup_folder='backups')
assert gc.plot_root == 'plots'
assert gc.png_dpi == 150
assert gc.backup_folder == 'backups'
def test_ui_config():
ui = config.UIConfig(max_recent_plots=10, search_debounce_ms=200)
assert ui.max_recent_plots == 10
assert ui.search_debounce_ms == 200
def test_metadata_config_defaults():
mc = config.MetadataConfig()
assert mc.cache_enabled is True
assert mc.inherit_from_parent is True
assert mc.supported_formats == ['.yaml', '.yml', '.json']
def test_metadata_config_custom():
mc = config.MetadataConfig(
cache_enabled=False,
inherit_from_parent=False,
supported_formats=['.yaml']
)
assert mc.cache_enabled is False
assert mc.inherit_from_parent is False
assert mc.supported_formats == ['.yaml']
def test_gallery_item():
item = config.GalleryItem(name="test", path=Path("/test/path"))
assert item.name == "test"
assert item.path == Path("/test/path")
def test_config_creation():
paths = config.PathConfig(work_dir="/work", web_folder="/web")
gallery = config.GalleryConfig(
plot_root="plots", png_dpi=300, backup_folder="backups")
ui = config.UIConfig(max_recent_plots=5, search_debounce_ms=100)
metadata = config.MetadataConfig()
cfg = config.Config(
paths=paths,
gallery=gallery,
ui=ui,
metadata=metadata
)
assert cfg.paths == paths
assert cfg.gallery == gallery
assert cfg.ui == ui
assert cfg.metadata == metadata
assert cfg.sources == []
def test_config_backward_compatibility_properties():
paths = config.PathConfig(work_dir="/work", web_folder="/web")
gallery = config.GalleryConfig(
plot_root="plots", png_dpi=300, backup_folder="backups")
ui = config.UIConfig(max_recent_plots=5, search_debounce_ms=100)
metadata = config.MetadataConfig()
cfg = config.Config(
paths=paths,
gallery=gallery,
ui=ui,
metadata=metadata
)
assert cfg.web_folder == "/web"
assert cfg.png_dpi == 300
assert cfg.plot_root == "plots"
assert cfg.backup_folder == "backups"
def test_config_from_yaml(tmp_path):
yaml_content = {
'paths': {
'work_dir': '/test/work',
'web_folder': '/test/web'
},
'gallery': {
'plot_root': 'test_plots',
'png_dpi': 200,
'backup_folder': 'test_backups'
},
'ui': {
'max_recent_plots': 15,
'search_debounce_ms': 300
},
'metadata': {
'cache_enabled': False,
'inherit_from_parent': False
},
'sources': [
{'name': 'source1', 'path': '/path1'},
{'name': 'source2', 'path': '/path2'}
]
}
yaml_file = tmp_path / 'test_config.yaml'
with yaml_file.open('w') as f:
yaml.dump(yaml_content, f)
cfg = config.Config.from_yaml(str(yaml_file))
assert cfg.paths.work_dir == '/test/work'
assert cfg.paths.web_folder == '/test/web'
assert cfg.gallery.plot_root == 'test_plots'
assert cfg.gallery.png_dpi == 200
assert cfg.ui.max_recent_plots == 15
assert cfg.metadata.cache_enabled is False
assert len(cfg.sources) == 2
assert cfg.sources[0].name == 'source1'
assert cfg.sources[0].path == Path('/path1')
def test_config_from_yaml_missing_file():
with pytest.raises(FileNotFoundError):
config.Config.from_yaml('/nonexistent/file.yaml')
def test_config_from_yaml_malformed():
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml',
delete=False) as f:
f.write('invalid: yaml: content: [')
f.flush()
with pytest.raises(yaml.YAMLError):
config.Config.from_yaml(f.name)
def test_config_to_yaml(tmp_path):
paths = config.PathConfig(work_dir="/work", web_folder="/web")
gallery = config.GalleryConfig(
plot_root="plots", png_dpi=300, backup_folder="backups")
ui = config.UIConfig(max_recent_plots=5, search_debounce_ms=100)
metadata = config.MetadataConfig()
sources = [config.GalleryItem(name="test", path=Path("/test"))]
cfg = config.Config(
paths=paths,
gallery=gallery,
ui=ui,
metadata=metadata,
sources=sources
)
yaml_file = tmp_path / 'output_config.yaml'
cfg.to_yaml(str(yaml_file))
assert yaml_file.exists()
# Just verify the file contains expected content (no Path parsing)
content = yaml_file.read_text()
assert 'work_dir: /work' in content
assert 'png_dpi: 300' in content
assert 'name: test' in content
def test_config_from_yaml_partial_data(tmp_path):
# Test with minimal YAML data
yaml_content = {
'paths': {'work_dir': '/min', 'web_folder': '/min_web'},
'gallery': {'plot_root': 'min_plots', 'png_dpi': 100,
'backup_folder': 'min_backup'},
'ui': {'max_recent_plots': 3, 'search_debounce_ms': 50}
}
yaml_file = tmp_path / 'minimal_config.yaml'
with yaml_file.open('w') as f:
yaml.dump(yaml_content, f)
cfg = config.Config.from_yaml(str(yaml_file))
# Should use defaults for metadata and empty sources
assert cfg.metadata.cache_enabled is True # default
assert cfg.sources == [] # default empty list
+138
View File
@@ -0,0 +1,138 @@
import pytest
import tempfile
import time
import os
from pathlib import Path
import subprocess
import sys
def test_python_version():
"""Test that Python 3.9+ is available."""
version = sys.version_info
assert version.major >= 3
assert version.minor >= 9
def test_required_modules():
"""Test that required Python modules are installed."""
try:
import jinja2 # noqa: F401
import yaml # noqa: F401
except ImportError as e:
pytest.fail(f"Required module not found: {e}")
def test_imagemagick_available():
"""Test that ImageMagick is installed and accessible."""
try:
result = subprocess.run(['convert', '-version'], capture_output=True, text=True, timeout=10)
assert result.returncode == 0
assert 'ImageMagick' in result.stdout
except (subprocess.TimeoutExpired, FileNotFoundError):
pytest.fail("ImageMagick not available or not working")
def test_format_file_size():
# Add current directory to path instead of /src
sys.path.insert(0, '.')
from gallery import format_file_size
assert format_file_size(0) == "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"
def test_needs_update():
sys.path.insert(0, '.')
from gallery import needs_update
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
source = temp_path / "source.txt"
target = temp_path / "target.txt"
source.write_text("test")
assert needs_update(source, target)
target.write_text("test")
time.sleep(0.1)
os.utime(target, (time.time(), time.time()))
assert not needs_update(source, target)
def test_metadata_loading():
sys.path.insert(0, '.')
from utils.metadata import load_metadata_file
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
yaml_file = temp_path / "test.yaml"
yaml_content = "title: Test\nauthor: Container Test\n"
yaml_file.write_text(yaml_content)
metadata = load_metadata_file(yaml_file)
assert metadata['title'] == 'Test'
assert metadata['author'] == 'Container Test'
def test_metadata_inheritance():
sys.path.insert(0, '.')
from utils.metadata import merge_metadata
parent = {'project': 'Test', 'version': '1.0'}
child = {'experiment': 'A', 'version': '1.1'}
merged = merge_metadata(parent, child)
assert merged['project'] == 'Test'
assert merged['experiment'] == 'A'
assert merged['version'] == '1.1'
def create_mock_pdf(path: Path):
path.write_text("%PDF-1.4\nMock PDF for testing")
def test_pdf_conversion(tmpdir):
sys.path.insert(0, '/src')
from gallery import convert_pdf_to_png, GalleryConfig
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
pdf_path = temp_path / "test.pdf"
create_mock_pdf(pdf_path)
try:
convert_pdf_to_png(pdf_path, GalleryConfig(tmpdir))
png_path = pdf_path.with_suffix('.png')
assert png_path.exists()
except subprocess.CalledProcessError:
pytest.skip("Mock PDF not processable by ImageMagick")
def create_test_structure(source_dir):
pdf_path = source_dir / "test_plot.pdf"
pdf_path.write_text("%PDF-1.4\nTest plot content")
metadata_path = source_dir / "metadata.yaml"
metadata_path.write_text("title: Container Test\nauthor: CI Pipeline\n")
def test_build_gallery(tmpdir):
sys.path.insert(0, '.')
from gallery import build_gallery, GalleryConfig
from unittest.mock import patch
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
source_dir = temp_path / "source"
web_dir = temp_path / "web"
source_dir.mkdir()
web_dir.mkdir()
create_test_structure(source_dir)
# Mock the PDF conversion and create the expected PNG file
def mock_convert_pdf_to_png(pdf_path):
png_path = pdf_path.with_suffix('.png')
png_path.write_text("Mock PNG content")
with patch('gallery.convert_pdf_to_png', side_effect=mock_convert_pdf_to_png):
try:
build_gallery(GalleryConfig(tmpdir), source_dir=source_dir, web_dir=web_dir)
html_file = web_dir / "index.html"
assert html_file.exists()
pdf_file = web_dir / "test_plot.pdf"
assert pdf_file.exists()
png_file = web_dir / "test_plot.png"
assert png_file.exists()
except Exception as e:
pytest.skip(f"Gallery generation failed: {e}")
+290
View File
@@ -0,0 +1,290 @@
import os
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest
from gallery import (
convert_pdf_to_png,
needs_update,
build_gallery,
calculate_directory_stats,
format_file_size,
datetime_from_timestamp,
strftime_filter
)
from datetime import datetime
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('subprocess.run')
def test_convert_pdf_to_png_success(mock_run, tmp_path):
from gallery import GalleryConfig
pdf_path = tmp_path / "test.pdf"
png_path = tmp_path / "test.png"
pdf_path.write_text("fake pdf")
mock_run.return_value = MagicMock(returncode=0)
convert_pdf_to_png(pdf_path, GalleryConfig(tmp_path))
mock_run.assert_called_once()
call_args = mock_run.call_args[0][0]
assert call_args[0] == "convert"
assert str(pdf_path) in call_args
assert str(png_path) in call_args
@patch('subprocess.run')
def test_convert_pdf_to_png_already_exists_newer(mock_run, 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 call subprocess since PNG is newer
mock_run.assert_not_called()
@patch('subprocess.run')
def test_convert_pdf_to_png_pdf_newer(mock_run, 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")
mock_run.return_value = MagicMock(returncode=0)
convert_pdf_to_png(pdf_path, GalleryConfig(tmp_path))
# Should call subprocess since PDF is newer
mock_run.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.render_gallery_page')
@patch('gallery.utils.metadata.save_metadata_cache')
@patch('gallery.utils.metadata.resolve_metadata_for_plot')
@patch('gallery.utils.metadata.merge_metadata')
@patch('gallery.utils.metadata.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,
mock_template,
tmp_path
):
from gallery import GalleryConfig
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"}
mock_template.render.return_value = "<html>test</html>"
build_gallery(GalleryConfig(tmp_path), source_dir, web_dir)
# Verify mocks were called
mock_convert.assert_called_once_with(pdf_file)
mock_copy.assert_called() # Should be called for PDF
mock_save_cache.assert_called_once()
# Check HTML file was created
html_file = web_dir / "index.html"
assert html_file.exists()
@patch('gallery.builder.render_gallery_page')
@patch('gallery.utils.metadata.save_metadata_cache')
@patch('gallery.utils.metadata.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()
+202
View File
@@ -0,0 +1,202 @@
import json
import pytest
from utils import metadata
import yaml
def test_load_metadata_file_yaml(tmp_path):
data = {'a': 1, 'b': 'test'}
yaml_path = tmp_path / 'meta.yaml'
yaml_path.write_text(yaml.dump(data))
result = metadata.load_metadata_file(yaml_path)
assert result == data
def test_load_metadata_file_json(tmp_path):
data = {'x': 42, 'y': 'hello'}
json_path = tmp_path / 'meta.json'
json_path.write_text(json.dumps(data))
result = metadata.load_metadata_file(json_path)
assert result == data
def test_load_metadata_file_missing(tmp_path):
missing_path = tmp_path / 'nope.yaml'
result = metadata.load_metadata_file(missing_path)
assert result == {}
def test_load_metadata_file_yml_extension(tmp_path):
data = {'test': 'yml_format'}
yml_path = tmp_path / 'meta.yml'
yml_path.write_text(yaml.dump(data))
result = metadata.load_metadata_file(yml_path)
assert result == data
def test_load_metadata_file_unknown_format(tmp_path):
txt_path = tmp_path / 'meta.txt'
txt_path.write_text('some text')
result = metadata.load_metadata_file(txt_path)
assert result == {}
def test_load_metadata_file_malformed_yaml(tmp_path):
yaml_path = tmp_path / 'bad.yaml'
yaml_path.write_text('invalid: yaml: content: [')
with pytest.raises(yaml.YAMLError):
metadata.load_metadata_file(yaml_path)
def test_load_metadata_file_malformed_json(tmp_path):
json_path = tmp_path / 'bad.json'
json_path.write_text('{"invalid": json}')
with pytest.raises(json.JSONDecodeError):
metadata.load_metadata_file(json_path)
def test_load_folder_metadata_yaml(tmp_path):
data = {'folder': 'metadata'}
metadata_path = tmp_path / 'metadata.yaml'
metadata_path.write_text(yaml.dump(data))
result = metadata.load_folder_metadata(tmp_path)
assert result == data
def test_load_folder_metadata_yml(tmp_path):
data = {'folder': 'metadata_yml'}
metadata_path = tmp_path / 'metadata.yml'
metadata_path.write_text(yaml.dump(data))
result = metadata.load_folder_metadata(tmp_path)
assert result == data
def test_load_folder_metadata_json(tmp_path):
data = {'folder': 'metadata_json'}
metadata_path = tmp_path / 'metadata.json'
metadata_path.write_text(json.dumps(data))
result = metadata.load_folder_metadata(tmp_path)
assert result == data
def test_load_folder_metadata_missing(tmp_path):
result = metadata.load_folder_metadata(tmp_path)
assert result == {}
def test_get_metadata_file_path_existing_yaml(tmp_path):
metadata_path = tmp_path / 'metadata.yaml'
metadata_path.write_text('test: data')
result = metadata.get_metadata_file_path(tmp_path)
assert result == str(metadata_path)
def test_get_metadata_file_path_existing_yml(tmp_path):
metadata_path = tmp_path / 'metadata.yml'
metadata_path.write_text('test: data')
result = metadata.get_metadata_file_path(tmp_path)
assert result == str(metadata_path)
def test_get_metadata_file_path_existing_json(tmp_path):
metadata_path = tmp_path / 'metadata.json'
metadata_path.write_text('{"test": "data"}')
result = metadata.get_metadata_file_path(tmp_path)
assert result == str(metadata_path)
def test_get_metadata_file_path_none_existing(tmp_path):
result = metadata.get_metadata_file_path(tmp_path)
expected = str(tmp_path / 'metadata.yaml')
assert result == expected
def test_merge_metadata():
parent = {'project': 'Test', 'version': '1.0', 'author': 'Parent'}
child = {'experiment': 'A', 'version': '1.1'}
merged = metadata.merge_metadata(parent, child)
expected = {'project': 'Test', 'version': '1.1',
'author': 'Parent', 'experiment': 'A'}
assert merged == expected
def test_merge_metadata_empty_parent():
parent = {}
child = {'experiment': 'A', 'version': '1.1'}
merged = metadata.merge_metadata(parent, child)
assert merged == child
def test_merge_metadata_empty_child():
parent = {'project': 'Test', 'version': '1.0'}
child = {}
merged = metadata.merge_metadata(parent, child)
assert merged == parent
def test_resolve_metadata_for_plot_with_specific_yaml(tmp_path):
# Create plot-specific metadata file
plot_path = tmp_path / 'test_plot.pdf'
plot_metadata_path = tmp_path / 'test_plot.yaml'
plot_metadata = {'specific': 'plot_data', 'override': 'plot_value'}
plot_metadata_path.write_text(yaml.dump(plot_metadata))
inherited = {'general': 'data', 'override': 'inherited_value'}
result = metadata.resolve_metadata_for_plot(plot_path, inherited)
expected = {'general': 'data',
'override': 'plot_value', 'specific': 'plot_data'}
assert result == expected
def test_resolve_metadata_for_plot_with_specific_json(tmp_path):
plot_path = tmp_path / 'test_plot.pdf'
plot_metadata_path = tmp_path / 'test_plot.json'
plot_metadata = {'specific': 'plot_data_json'}
plot_metadata_path.write_text(json.dumps(plot_metadata))
inherited = {'general': 'data'}
result = metadata.resolve_metadata_for_plot(plot_path, inherited)
expected = {'general': 'data', 'specific': 'plot_data_json'}
assert result == expected
def test_resolve_metadata_for_plot_no_specific(tmp_path):
plot_path = tmp_path / 'test_plot.pdf'
inherited = {'general': 'data', 'inherited': 'value'}
result = metadata.resolve_metadata_for_plot(plot_path, inherited)
# Should return copy of inherited metadata
assert result == inherited
assert result is not inherited # Should be a copy
def test_save_metadata_cache(tmp_path):
cache_data = {
'plot1': {'title': 'Plot 1', 'author': 'Test'},
'plot2': {'title': 'Plot 2', 'experiment': 'B'}
}
metadata.save_metadata_cache(tmp_path, cache_data)
cache_file = tmp_path / 'meta_cache.json'
assert cache_file.exists()
with cache_file.open('r') as f:
loaded_data = json.load(f)
assert loaded_data == cache_data
def test_save_metadata_cache_empty(tmp_path):
cache_data = {}
metadata.save_metadata_cache(tmp_path, cache_data)
cache_file = tmp_path / 'meta_cache.json'
assert cache_file.exists()
with cache_file.open('r') as f:
loaded_data = json.load(f)
assert loaded_data == {}