Add pytest and gitlab-ci for container

This commit is contained in:
Kylian Schmidt
2025-09-08 14:47:38 +02:00
parent 9e59d06796
commit 802fee1337
10 changed files with 263 additions and 217 deletions
+1
View File
@@ -2,3 +2,4 @@
.vscode
*.sif
*.ipynb
backups
+10 -213
View File
@@ -1,82 +1,49 @@
# GitLab CI/CD Pipeline for Gallery Generator
# Tests container building and runs unit tests inside Apptainer
# Builds Apptainer container and runs tests inside it
stages:
- build
- test
- deploy
variables:
CONTAINER_IMAGE: "gallery-generator.sif"
APPTAINER_CACHE_DIR: "$CI_PROJECT_DIR/.apptainer-cache"
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
# Cache to speed up builds
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .apptainer-cache/
- .pip-cache/
# Build the Apptainer container
build:container:
stage: build
image: ubuntu:22.04
before_script:
# Install Apptainer
- apt-get update -qq
- apt-get install -y wget
- wget -O- http://neuro.debian.net/lists/jammy.us-ca.full | tee /etc/apt/sources.list.d/neurodebian.sources.list
- apt-key adv --recv-keys --keyserver hkps://keyserver.ubuntu.com 0xA5D32F012649A5A9
- apt-get update -qq
- apt-get install -y apptainer
# Create cache directory
- mkdir -p $APPTAINER_CACHE_DIR
script:
- echo "Building Apptainer container..."
- apptainer --version
- apptainer build $CONTAINER_IMAGE Singularity.def
- ls -lh $CONTAINER_IMAGE
- echo "Container built successfully"
artifacts:
paths:
- $CONTAINER_IMAGE
expire_in: 1 hour
only:
- main
- merge_requests
- develop
# Test container build process
test:container-build:
# Run tests inside the container
test:pytest:
stage: test
image: ubuntu:22.04
dependencies:
- build:container
before_script:
# Install Apptainer
- apt-get update -qq
- apt-get install -y wget
- wget -O- http://neuro.debian.net/lists/jammy.us-ca.full | tee /etc/apt/sources.list.d/neurodebian.sources.list
- apt-key adv --recv-keys --keyserver hkps://keyserver.ubuntu.com 0xA5D32F012649A5A9
- apt-get update -qq
- apt-get install -y apptainer python3
script:
- echo "Testing container build process..."
- python3 tests/test_build_container.py
only:
- main
- merge_requests
- develop
# Run tests inside the container with coverage
test:unit-tests:
stage: test
image: ubuntu:22.04
dependencies:
- build:container
before_script:
# Install Apptainer
- apt-get update -qq
- apt-get install -y wget
- wget -O- http://neuro.debian.net/lists/jammy.us-ca.full | tee /etc/apt/sources.list.d/neurodebian.sources.list
@@ -84,31 +51,18 @@ test:unit-tests:
- apt-get update -qq
- apt-get install -y apptainer
script:
- echo "Running unit tests inside container..."
- apptainer --version
- ls -la $CONTAINER_IMAGE
# Test that container works
- apptainer exec $CONTAINER_IMAGE python3 --version
- apptainer exec $CONTAINER_IMAGE pip list
# Run the container test suite
- apptainer exec $CONTAINER_IMAGE python3 /src/tests/test_container.py
- echo "Running pytest inside container..."
- apptainer exec $CONTAINER_IMAGE pytest /src/tests/ -v
artifacts:
reports:
junit: test-results.xml
when: always
only:
- main
- merge_requests
- develop
# Run automated coverage testing
# Run tests with coverage
test:coverage:
stage: test
image: ubuntu:22.04
dependencies:
- build:container
before_script:
# Install Apptainer
- apt-get update -qq
- apt-get install -y wget
- wget -O- http://neuro.debian.net/lists/jammy.us-ca.full | tee /etc/apt/sources.list.d/neurodebian.sources.list
@@ -116,15 +70,10 @@ test:coverage:
- apt-get update -qq
- apt-get install -y apptainer
script:
- echo "Running automated coverage analysis..."
- apptainer --version
- ls -la $CONTAINER_IMAGE
# Run automated coverage testing
- apptainer exec $CONTAINER_IMAGE python3 /src/tests/run_coverage.py
# Extract coverage report from container
- apptainer exec $CONTAINER_IMAGE cat /src/coverage.xml > coverage.xml || true
- apptainer exec $CONTAINER_IMAGE ls -la /src/coverage_html_report/ || true
coverage: '/TOTAL.+?(\d+\.\d+)%/'
- echo "Running coverage analysis inside container..."
- apptainer exec $CONTAINER_IMAGE pytest /src/tests/ --cov=/src --cov-report=xml --cov-report=term
- apptainer exec $CONTAINER_IMAGE cat /src/coverage.xml > coverage.xml || echo "No coverage.xml found"
coverage: '/TOTAL.*\s+(\d+%)$/'
artifacts:
reports:
coverage_report:
@@ -133,155 +82,3 @@ test:coverage:
paths:
- coverage.xml
expire_in: 30 days
only:
- main
- merge_requests
- develop
# Test gallery generation functionality
test:gallery-generation:
stage: test
image: ubuntu:22.04
dependencies:
- build:container
before_script:
# Install Apptainer
- apt-get update -qq
- apt-get install -y wget
- wget -O- http://neuro.debian.net/lists/jammy.us-ca.full | tee /etc/apt/sources.list.d/neurodebian.sources.list
- apt-key adv --recv-keys --keyserver hkps://keyserver.ubuntu.com 0xA5D32F012649A5A9
- apt-get update -qq
- apt-get install -y apptainer
script:
- echo "Testing gallery generation..."
# Create test data
- mkdir -p test_input
- echo "%PDF-1.4" > test_input/test.pdf
- echo "title: Test Gallery" > test_input/metadata.yaml
# Test the main script
- apptainer exec $CONTAINER_IMAGE python3 /src/generate_gallery.py --help || true
- echo "Gallery generation test completed"
only:
- main
- merge_requests
- develop
# Performance and integration tests
test:performance:
stage: test
image: ubuntu:22.04
dependencies:
- build:container
before_script:
- apt-get update -qq
- apt-get install -y wget time
- wget -O- http://neuro.debian.net/lists/jammy.us-ca.full | tee /etc/apt/sources.list.d/neurodebian.sources.list
- apt-key adv --recv-keys --keyserver hkps://keyserver.ubuntu.com 0xA5D32F012649A5A9
- apt-get update -qq
- apt-get install -y apptainer
script:
- echo "Running performance tests..."
# Test container startup time
- time apptainer exec $CONTAINER_IMAGE python3 -c "print('Container startup test')"
# Test memory usage
- apptainer exec $CONTAINER_IMAGE python3 -c "
import psutil;
print(f'Memory usage: {psutil.virtual_memory().percent}%')"
- echo "Performance tests completed"
only:
- main
- merge_requests
allow_failure: true
# Security scan (optional)
test:security:
stage: test
image: ubuntu:22.04
dependencies:
- build:container
before_script:
- apt-get update -qq
- apt-get install -y wget
- wget -O- http://neuro.debian.net/lists/jammy.us-ca.full | tee /etc/apt/sources.list.d/neurodebian.sources.list
- apt-key adv --recv-keys --keyserver hkps://keyserver.ubuntu.com 0xA5D32F012649A5A9
- apt-get update -qq
- apt-get install -y apptainer
script:
- echo "Running basic security checks..."
# Check for known vulnerabilities in base image
- apptainer exec $CONTAINER_IMAGE python3 -c "
import sys;
print(f'Python version: {sys.version}');
import subprocess;
result = subprocess.run(['pip', 'list'], capture_output=True, text=True);
print('Installed packages:');
print(result.stdout)"
- echo "Security scan completed"
only:
- main
- merge_requests
allow_failure: true
# Documentation and examples
test:documentation:
stage: test
image: python:3.11-slim
script:
- echo "Validating documentation..."
- python3 -c "
import pathlib;
docs = ['README.md', 'tests/README.md', 'Singularity.def'];
for doc in docs:
if pathlib.Path(doc).exists():
print(f'✅ {doc} exists');
else:
print(f'❌ {doc} missing')"
- echo "Documentation validation completed"
only:
- main
- merge_requests
# Deploy (if needed)
deploy:registry:
stage: deploy
image: ubuntu:22.04
dependencies:
- build:container
before_script:
- apt-get update -qq
- apt-get install -y wget
- wget -O- http://neuro.debian.net/lists/jammy.us-ca.full | tee /etc/apt/sources.list.d/neurodebian.sources.list
- apt-key adv --recv-keys --keyserver hkps://keyserver.ubuntu.com 0xA5D32F012649A5A9
- apt-get update -qq
- apt-get install -y apptainer
script:
- echo "Deploying container to registry (placeholder)..."
- echo "Container size:"
- ls -lh $CONTAINER_IMAGE
- echo "Container would be pushed to registry here"
# Actual deployment would push to a container registry
# - apptainer push $CONTAINER_IMAGE oras://registry.example.com/gallery-generator:latest
only:
- main
when: manual
# Job for creating releases
create:release:
stage: deploy
image: ubuntu:22.04
dependencies:
- build:container
script:
- echo "Creating release artifacts..."
- mkdir -p release/
- cp $CONTAINER_IMAGE release/
- cp Singularity.def release/
- cp tests/test_container.py release/
- echo "Release artifacts created"
artifacts:
paths:
- release/
expire_in: 1 week
only:
- tags
when: manual
+4
View File
@@ -0,0 +1,4 @@
import sys
sys.path.append("..")
+42
View File
@@ -0,0 +1,42 @@
import zipfile
import datetime
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)
# Manually execute the backup logic
today = datetime.date.today().strftime('%Y%m%d')
backup_name = f'backup-{today}.zip'
backup_path = backup_folder / backup_name
# Remove if exists
if backup_path.exists():
backup_path.unlink()
# Create backup manually using the backup module's logic
with zipfile.ZipFile(backup_path, "w", zipfile.ZIP_DEFLATED) as zipf:
for path in web_folder.rglob("*"):
if path.is_file():
arcname = path.relative_to(web_folder.parent)
zipf.write(path, arcname)
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()
View File
+27
View File
@@ -0,0 +1,27 @@
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
+150
View File
@@ -0,0 +1,150 @@
import pytest
import tempfile
import time
import os
from pathlib import Path
import subprocess
import sys
def test_python_version():
"""Test that Python 3.11+ is available."""
version = sys.version_info
assert version.major >= 3
assert version.minor >= 11
def test_required_modules():
"""Test that required Python modules are installed."""
try:
import jinja2 # type: ignore
import yaml # type: ignore
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_working_directory():
"""Test that the source code is available."""
expected_files = ['generate_gallery.py',
'config.yaml', 'utils/']
for file_path in expected_files:
# Check current directory instead of /src
path = Path('.') / file_path
assert path.exists(), f"Missing: {file_path}"
def test_format_file_size():
# Add current directory to path instead of /src
sys.path.insert(0, '.')
from generate_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 generate_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():
sys.path.insert(0, '/src')
from generate_gallery import convert_pdf_to_png
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)
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():
sys.path.insert(0, '.')
from generate_gallery import build_gallery
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('generate_gallery.convert_pdf_to_png',
side_effect=mock_convert_pdf_to_png):
try:
build_gallery(source_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.fail(f"Gallery generation failed: {e}")
+25
View File
@@ -0,0 +1,25 @@
import json
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 == {}