diff --git a/.gitignore b/.gitignore index ad429e4..1e18cb9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .vscode *.sif *.ipynb -backups \ No newline at end of file +backups +.pytest_cache \ No newline at end of file diff --git a/tests/test_backup.py b/tests/test_backup.py index d21e289..635bed4 100644 --- a/tests/test_backup.py +++ b/tests/test_backup.py @@ -1,5 +1,6 @@ import zipfile import datetime +from unittest.mock import patch from utils import backup @@ -31,4 +32,183 @@ def test_backup_creates_zip(tmp_path, monkeypatch): assert any('file2.txt' in n for n in names) # Cleanup: remove the backup file after test - backup_path.unlink() \ No newline at end of file + 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() \ No newline at end of file diff --git a/tests/test_build_container.py b/tests/test_build_container.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_config.py b/tests/test_config.py index 2343c20..ed2d134 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,3 +1,7 @@ +from pathlib import Path +import tempfile +import pytest +import yaml from utils import config @@ -25,3 +29,169 @@ 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() + + # Load back and verify + with yaml_file.open('r') as f: + loaded_data = yaml.safe_load(f) + + assert loaded_data['paths']['work_dir'] == "/work" + assert loaded_data['gallery']['png_dpi'] == 300 + assert len(loaded_data['sources']) == 1 + + +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 diff --git a/tests/test_container.py b/tests/test_container.py index 8aacee4..106980a 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -17,8 +17,8 @@ def test_python_version(): def test_required_modules(): """Test that required Python modules are installed.""" try: - import jinja2 # type: ignore - import yaml # type: ignore + import jinja2 # noqa: F401 + import yaml # noqa: F401 except ImportError as e: pytest.fail(f"Required module not found: {e}") diff --git a/tests/test_generate_gallery.py b/tests/test_generate_gallery.py new file mode 100644 index 0000000..dade440 --- /dev/null +++ b/tests/test_generate_gallery.py @@ -0,0 +1,279 @@ +from pathlib import Path +from unittest.mock import patch, MagicMock +from generate_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") + + assert needs_update(source, target) is True + + +@patch('subprocess.run') +def test_convert_pdf_to_png_success(mock_run, tmp_path): + 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) + + 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): + 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 + import time + time.sleep(0.1) + png_path.touch() + + convert_pdf_to_png(pdf_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): + 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) + + # 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('generate_gallery.template') +@patch('generate_gallery.save_metadata_cache') +@patch('generate_gallery.resolve_metadata_for_plot') +@patch('generate_gallery.merge_metadata') +@patch('generate_gallery.load_folder_metadata') +@patch('generate_gallery.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 +): + 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") + + # Create corresponding PNG + png_file = source_dir / "test.png" + png_file.write_text("fake png content") + + # 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 = "test" + + build_gallery(source_dir, web_dir) + + # Verify mocks were called + mock_load_folder.assert_called_once_with(source_dir) + mock_convert.assert_called_once_with(pdf_file) + mock_copy.assert_called() # Should be called for PDF and PNG + mock_save_cache.assert_called_once() + + # Check HTML file was created + html_file = web_dir / "index.html" + assert html_file.exists() + + +@patch('generate_gallery.template') +@patch('generate_gallery.save_metadata_cache') +@patch('generate_gallery.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 = "test" + + build_gallery(source_dir, web_dir) + + # Check subdirectory was created in web + web_subdir = web_dir / "subdir" + assert web_subdir.exists() + assert web_subdir.is_dir() + + +@patch('generate_gallery.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 + + with patch('generate_gallery.template') as mock_template: + mock_template.render.return_value = "test" + build_gallery(source_dir, web_dir) + + # copy2 should not be called since files are up to date + mock_copy.assert_not_called() diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 7a768d6..5dc8a70 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,4 +1,5 @@ import json +import pytest from utils import metadata import yaml @@ -22,4 +23,180 @@ def test_load_metadata_file_json(tmp_path): def test_load_metadata_file_missing(tmp_path): missing_path = tmp_path / 'nope.yaml' result = metadata.load_metadata_file(missing_path) - assert result == {} \ No newline at end of file + 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 == {}