Rewrite CI to lint/typecheck/audit/test only; add HPC deployment path
- 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>
This commit is contained in:
+155
-163
@@ -1,195 +1,187 @@
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from utils import config
|
||||
|
||||
from gallery.config import ConfigManager, GalleryConfig, GalleryDefaults, GallerySource
|
||||
|
||||
|
||||
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_source_path_conversion():
|
||||
source = GallerySource(name="test", path="/test/path")
|
||||
assert source.name == "test"
|
||||
assert source.path == Path("/test/path")
|
||||
|
||||
|
||||
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_gallery_defaults():
|
||||
defaults = GalleryDefaults()
|
||||
assert defaults.png_dpi == 400
|
||||
assert defaults.plot_root == "gallery"
|
||||
assert defaults.cache_enabled is True
|
||||
assert defaults.inherit_from_parent is True
|
||||
|
||||
|
||||
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_gallery_config_defaults():
|
||||
cfg = GalleryConfig(web_folder="/web")
|
||||
assert cfg.web_folder == Path("/web")
|
||||
assert cfg.sources == []
|
||||
assert cfg.png_dpi == GalleryDefaults.png_dpi
|
||||
assert cfg.plot_root == GalleryDefaults.plot_root
|
||||
assert cfg.cache_enabled == GalleryDefaults.cache_enabled
|
||||
assert cfg.inherit_from_parent == GalleryDefaults.inherit_from_parent
|
||||
assert cfg.backup_folder == ""
|
||||
|
||||
|
||||
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_gallery_config_sources_from_dicts():
|
||||
cfg = GalleryConfig(web_folder="/web", sources=[{"name": "s1", "path": "/p1"}])
|
||||
assert len(cfg.sources) == 1
|
||||
assert isinstance(cfg.sources[0], GallerySource)
|
||||
assert cfg.sources[0].name == "s1"
|
||||
assert cfg.sources[0].path == Path("/p1")
|
||||
|
||||
|
||||
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_config_sources_invalid_type():
|
||||
with pytest.raises(TypeError):
|
||||
GalleryConfig(web_folder="/web", sources=[123])
|
||||
|
||||
|
||||
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_gallery_config_from_yaml(tmp_path):
|
||||
yaml_content = {
|
||||
"paths": {"web_folder": "/test/web"},
|
||||
"gallery": {"plot_root": "test_plots", "png_dpi": 200, "backup_folder": "test_backups"},
|
||||
"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 = GalleryConfig.from_yaml(yaml_file)
|
||||
|
||||
assert cfg.web_folder == Path("/test/web")
|
||||
assert cfg.plot_root == "test_plots"
|
||||
assert cfg.png_dpi == 200
|
||||
assert cfg.backup_folder == "test_backups"
|
||||
assert cfg.cache_enabled is False
|
||||
assert cfg.inherit_from_parent is False
|
||||
assert len(cfg.sources) == 2
|
||||
assert cfg.sources[0].name == "source1"
|
||||
assert cfg.sources[0].path == Path("/path1")
|
||||
|
||||
|
||||
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()
|
||||
def test_gallery_config_from_yaml_missing_file():
|
||||
with pytest.raises(FileNotFoundError):
|
||||
GalleryConfig.from_yaml("/nonexistent/file.yaml")
|
||||
|
||||
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
|
||||
def test_gallery_config_from_yaml_missing_web_folder(tmp_path):
|
||||
yaml_file = tmp_path / "no_web_folder.yaml"
|
||||
yaml_file.write_text(yaml.dump({"gallery": {"plot_root": "plots"}}))
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
GalleryConfig.from_yaml(yaml_file)
|
||||
|
||||
|
||||
def test_gallery_config_from_yaml_partial_data(tmp_path):
|
||||
yaml_content = {"paths": {"web_folder": "/min_web"}}
|
||||
|
||||
yaml_file = tmp_path / "minimal_config.yaml"
|
||||
yaml_file.write_text(yaml.dump(yaml_content))
|
||||
|
||||
cfg = GalleryConfig.from_yaml(yaml_file)
|
||||
|
||||
assert cfg.web_folder == Path("/min_web")
|
||||
assert cfg.png_dpi == GalleryDefaults.png_dpi
|
||||
assert cfg.plot_root == GalleryDefaults.plot_root
|
||||
assert cfg.cache_enabled is True
|
||||
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
|
||||
def test_gallery_config_to_yaml_round_trip(tmp_path):
|
||||
cfg = GalleryConfig(
|
||||
web_folder="/web",
|
||||
sources=[{"name": "test", "path": "/test"}],
|
||||
plot_root="plots",
|
||||
png_dpi=300,
|
||||
backup_folder="backups",
|
||||
)
|
||||
|
||||
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))
|
||||
yaml_file = tmp_path / "output_config.yaml"
|
||||
cfg.to_yaml(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
|
||||
reloaded = GalleryConfig.from_yaml(yaml_file)
|
||||
assert reloaded.web_folder == cfg.web_folder
|
||||
assert reloaded.plot_root == cfg.plot_root
|
||||
assert reloaded.png_dpi == cfg.png_dpi
|
||||
assert reloaded.backup_folder == cfg.backup_folder
|
||||
assert reloaded.sources[0].name == "test"
|
||||
|
||||
|
||||
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}
|
||||
}
|
||||
# ---------------------------------------------------------------------------
|
||||
# ConfigManager
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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))
|
||||
@pytest.fixture
|
||||
def config_manager(tmp_path):
|
||||
path = tmp_path / "config.yaml"
|
||||
path.write_text(
|
||||
yaml.dump(
|
||||
{
|
||||
"paths": {"web_folder": "/web"},
|
||||
"gallery": {"plot_root": "gallery", "png_dpi": 400},
|
||||
"sources": [{"name": "existing", "path": "/existing"}],
|
||||
}
|
||||
)
|
||||
)
|
||||
return ConfigManager(path)
|
||||
|
||||
# Should use defaults for metadata and empty sources
|
||||
assert cfg.metadata.cache_enabled is True # default
|
||||
assert cfg.sources == [] # default empty list
|
||||
|
||||
def test_config_manager_get(config_manager):
|
||||
assert config_manager.get("gallery.png_dpi") == 400
|
||||
|
||||
|
||||
def test_config_manager_get_missing_key(config_manager):
|
||||
with pytest.raises(KeyError):
|
||||
config_manager.get("gallery.nonexistent")
|
||||
|
||||
|
||||
def test_config_manager_set(config_manager):
|
||||
config_manager.set("gallery.png_dpi", "600")
|
||||
assert config_manager.get("gallery.png_dpi") == 600
|
||||
|
||||
|
||||
def test_config_manager_list_all(config_manager):
|
||||
data = config_manager.list_all()
|
||||
assert data["paths"]["web_folder"] == "/web"
|
||||
|
||||
|
||||
def test_config_manager_add_source(config_manager):
|
||||
config_manager.add_source("new_source", "/new/path")
|
||||
sources = config_manager.list_sources()
|
||||
assert {"name": "new_source", "path": "/new/path"} in sources
|
||||
|
||||
|
||||
def test_config_manager_add_source_duplicate(config_manager):
|
||||
with pytest.raises(ValueError):
|
||||
config_manager.add_source("existing", "/other/path")
|
||||
|
||||
|
||||
def test_config_manager_remove_source(config_manager):
|
||||
config_manager.remove_source("existing")
|
||||
assert config_manager.list_sources() == []
|
||||
|
||||
|
||||
def test_config_manager_remove_source_not_found(config_manager):
|
||||
with pytest.raises(KeyError):
|
||||
config_manager.remove_source("nonexistent")
|
||||
|
||||
|
||||
def test_config_manager_list_sources(config_manager):
|
||||
sources = config_manager.list_sources()
|
||||
assert sources == [{"name": "existing", "path": "/existing"}]
|
||||
|
||||
Reference in New Issue
Block a user