Clean up unittest attempt.

This commit is contained in:
Kylian Schmidt
2025-09-08 12:53:16 +02:00
parent 657b273975
commit 9e59d06796
22 changed files with 17 additions and 7476 deletions
BIN
View File
Binary file not shown.
-36
View File
@@ -1,36 +0,0 @@
[run]
source = .
omit =
tests/*
__pycache__/*
.git/*
assets/*
docs/*
examples/*
templates/*
.coverage*
setup.py
conftest.py
[report]
precision = 2
show_missing = True
skip_covered = False
exclude_lines =
pragma: no cover
def __repr__
if self.debug:
if settings.DEBUG
raise AssertionError
raise NotImplementedError
if 0:
if __name__ == .__main__.:
class .*\bProtocol\):
@(abc\.)?abstractmethod
[html]
directory = coverage_html_report
title = Gallery Generator Coverage Report
[xml]
output = coverage.xml
-421
View File
@@ -1,421 +0,0 @@
<?xml version="1.0" ?>
<coverage version="7.10.6" timestamp="1757321291920" lines-valid="379" lines-covered="128" line-rate="0.3377" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.10.6 -->
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
<sources>
<source></source>
</sources>
<packages>
<package name="." line-rate="0.9437" branch-rate="0" complexity="0">
<classes>
<class name="test_simple_coverage.py" filename="test_simple_coverage.py" complexity="0" line-rate="0.9437" branch-rate="0">
<methods/>
<lines>
<line number="5" hits="1"/>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
<line number="8" hits="1"/>
<line number="9" hits="1"/>
<line number="12" hits="1"/>
<line number="13" hits="1"/>
<line number="16" hits="1"/>
<line number="19" hits="1"/>
<line number="22" hits="1"/>
<line number="23" hits="1"/>
<line number="24" hits="1"/>
<line number="25" hits="1"/>
<line number="26" hits="1"/>
<line number="27" hits="1"/>
<line number="29" hits="1"/>
<line number="31" hits="1"/>
<line number="32" hits="1"/>
<line number="35" hits="1"/>
<line number="40" hits="1"/>
<line number="41" hits="1"/>
<line number="44" hits="1"/>
<line number="47" hits="1"/>
<line number="53" hits="1"/>
<line number="54" hits="1"/>
<line number="57" hits="1"/>
<line number="62" hits="1"/>
<line number="63" hits="1"/>
<line number="65" hits="0"/>
<line number="66" hits="0"/>
<line number="68" hits="1"/>
<line number="70" hits="1"/>
<line number="71" hits="1"/>
<line number="74" hits="1"/>
<line number="75" hits="1"/>
<line number="77" hits="1"/>
<line number="80" hits="1"/>
<line number="82" hits="1"/>
<line number="84" hits="1"/>
<line number="87" hits="1"/>
<line number="88" hits="1"/>
<line number="89" hits="1"/>
<line number="92" hits="1"/>
<line number="93" hits="1"/>
<line number="94" hits="1"/>
<line number="95" hits="1"/>
<line number="97" hits="1"/>
<line number="99" hits="0"/>
<line number="100" hits="0"/>
<line number="102" hits="1"/>
<line number="105" hits="1"/>
<line number="107" hits="1"/>
<line number="109" hits="1"/>
<line number="110" hits="1"/>
<line number="113" hits="1"/>
<line number="114" hits="1"/>
<line number="115" hits="1"/>
<line number="118" hits="1"/>
<line number="119" hits="1"/>
<line number="122" hits="1"/>
<line number="124" hits="1"/>
<line number="127" hits="1"/>
<line number="128" hits="1"/>
<line number="131" hits="1"/>
<line number="132" hits="1"/>
<line number="135" hits="1"/>
<line number="136" hits="1"/>
<line number="139" hits="1"/>
<line number="140" hits="1"/>
<line number="143" hits="1"/>
<line number="144" hits="1"/>
</lines>
</class>
</classes>
</package>
<package name=".work.kschmidt.web" line-rate="0" branch-rate="0" complexity="0">
<classes>
<class name="generate_gallery.py" filename="/work/kschmidt/web/generate_gallery.py" complexity="0" line-rate="0" branch-rate="0">
<methods/>
<lines>
<line number="16" hits="0"/>
<line number="17" hits="0"/>
<line number="18" hits="0"/>
<line number="19" hits="0"/>
<line number="20" hits="0"/>
<line number="21" hits="0"/>
<line number="22" hits="0"/>
<line number="23" hits="0"/>
<line number="25" hits="0"/>
<line number="26" hits="0"/>
<line number="35" hits="0"/>
<line number="38" hits="0"/>
<line number="40" hits="0"/>
<line number="43" hits="0"/>
<line number="45" hits="0"/>
<line number="48" hits="0"/>
<line number="49" hits="0"/>
<line number="50" hits="0"/>
<line number="51" hits="0"/>
<line number="54" hits="0"/>
<line number="67" hits="0"/>
<line number="69" hits="0"/>
<line number="70" hits="0"/>
<line number="71" hits="0"/>
<line number="72" hits="0"/>
<line number="73" hits="0"/>
<line number="75" hits="0"/>
<line number="77" hits="0"/>
<line number="78" hits="0"/>
<line number="87" hits="0"/>
<line number="98" hits="0"/>
<line number="99" hits="0"/>
<line number="101" hits="0"/>
<line number="102" hits="0"/>
<line number="104" hits="0"/>
<line number="107" hits="0"/>
<line number="123" hits="0"/>
<line number="124" hits="0"/>
<line number="126" hits="0"/>
<line number="127" hits="0"/>
<line number="130" hits="0"/>
<line number="131" hits="0"/>
<line number="133" hits="0"/>
<line number="135" hits="0"/>
<line number="137" hits="0"/>
<line number="138" hits="0"/>
<line number="140" hits="0"/>
<line number="141" hits="0"/>
<line number="143" hits="0"/>
<line number="144" hits="0"/>
<line number="146" hits="0"/>
<line number="147" hits="0"/>
<line number="148" hits="0"/>
<line number="150" hits="0"/>
<line number="152" hits="0"/>
<line number="153" hits="0"/>
<line number="155" hits="0"/>
<line number="156" hits="0"/>
<line number="157" hits="0"/>
<line number="159" hits="0"/>
<line number="162" hits="0"/>
<line number="163" hits="0"/>
<line number="166" hits="0"/>
<line number="168" hits="0"/>
<line number="177" hits="0"/>
<line number="179" hits="0"/>
<line number="180" hits="0"/>
<line number="181" hits="0"/>
<line number="182" hits="0"/>
<line number="183" hits="0"/>
<line number="185" hits="0"/>
<line number="186" hits="0"/>
<line number="188" hits="0"/>
<line number="192" hits="0"/>
<line number="193" hits="0"/>
<line number="194" hits="0"/>
<line number="196" hits="0"/>
<line number="197" hits="0"/>
<line number="198" hits="0"/>
<line number="199" hits="0"/>
<line number="201" hits="0"/>
<line number="202" hits="0"/>
<line number="204" hits="0"/>
<line number="207" hits="0"/>
<line number="208" hits="0"/>
<line number="216" hits="0"/>
<line number="217" hits="0"/>
<line number="220" hits="0"/>
<line number="221" hits="0"/>
<line number="223" hits="0"/>
<line number="224" hits="0"/>
<line number="237" hits="0"/>
<line number="239" hits="0"/>
<line number="242" hits="0"/>
<line number="252" hits="0"/>
<line number="260" hits="0"/>
<line number="261" hits="0"/>
<line number="263" hits="0"/>
<line number="264" hits="0"/>
<line number="265" hits="0"/>
<line number="266" hits="0"/>
<line number="267" hits="0"/>
<line number="269" hits="0"/>
<line number="270" hits="0"/>
<line number="271" hits="0"/>
<line number="272" hits="0"/>
<line number="273" hits="0"/>
<line number="274" hits="0"/>
<line number="276" hits="0"/>
<line number="279" hits="0"/>
<line number="289" hits="0"/>
<line number="290" hits="0"/>
<line number="292" hits="0"/>
<line number="293" hits="0"/>
<line number="294" hits="0"/>
<line number="295" hits="0"/>
<line number="296" hits="0"/>
<line number="297" hits="0"/>
<line number="299" hits="0"/>
<line number="302" hits="0"/>
<line number="307" hits="0"/>
<line number="308" hits="0"/>
<line number="309" hits="0"/>
<line number="310" hits="0"/>
<line number="311" hits="0"/>
<line number="312" hits="0"/>
<line number="313" hits="0"/>
<line number="314" hits="0"/>
<line number="317" hits="0"/>
<line number="327" hits="0"/>
<line number="329" hits="0"/>
<line number="330" hits="0"/>
<line number="331" hits="0"/>
<line number="334" hits="0"/>
<line number="337" hits="0"/>
<line number="338" hits="0"/>
<line number="340" hits="0"/>
<line number="342" hits="0"/>
<line number="343" hits="0"/>
<line number="344" hits="0"/>
<line number="345" hits="0"/>
<line number="346" hits="0"/>
<line number="347" hits="0"/>
<line number="348" hits="0"/>
<line number="350" hits="0"/>
<line number="352" hits="0"/>
<line number="353" hits="0"/>
<line number="355" hits="0"/>
<line number="356" hits="0"/>
<line number="357" hits="0"/>
<line number="359" hits="0"/>
<line number="360" hits="0"/>
<line number="362" hits="0"/>
<line number="363" hits="0"/>
<line number="364" hits="0"/>
<line number="366" hits="0"/>
<line number="367" hits="0"/>
<line number="368" hits="0"/>
<line number="370" hits="0"/>
<line number="372" hits="0"/>
<line number="373" hits="0"/>
<line number="375" hits="0"/>
<line number="376" hits="0"/>
<line number="377" hits="0"/>
<line number="379" hits="0"/>
<line number="382" hits="0"/>
<line number="384" hits="0"/>
<line number="392" hits="0"/>
<line number="393" hits="0"/>
<line number="402" hits="0"/>
<line number="404" hits="0"/>
<line number="405" hits="0"/>
<line number="406" hits="0"/>
<line number="420" hits="0"/>
<line number="421" hits="0"/>
<line number="423" hits="0"/>
<line number="424" hits="0"/>
<line number="425" hits="0"/>
<line number="426" hits="0"/>
<line number="427" hits="0"/>
<line number="429" hits="0"/>
<line number="432" hits="0"/>
<line number="435" hits="0"/>
<line number="436" hits="0"/>
<line number="437" hits="0"/>
<line number="439" hits="0"/>
<line number="441" hits="0"/>
<line number="442" hits="0"/>
<line number="447" hits="0"/>
<line number="448" hits="0"/>
</lines>
</class>
</classes>
</package>
<package name=".work.kschmidt.web.orchestration" line-rate="0.5169" branch-rate="0" complexity="0">
<classes>
<class name="config.py" filename="/work/kschmidt/web/orchestration/config.py" complexity="0" line-rate="0.6769" branch-rate="0">
<methods/>
<lines>
<line number="9" hits="1"/>
<line number="10" hits="1"/>
<line number="11" hits="1"/>
<line number="14" hits="1"/>
<line number="15" hits="1"/>
<line number="17" hits="1"/>
<line number="18" hits="1"/>
<line number="21" hits="1"/>
<line number="22" hits="1"/>
<line number="24" hits="1"/>
<line number="25" hits="1"/>
<line number="26" hits="1"/>
<line number="29" hits="1"/>
<line number="30" hits="1"/>
<line number="32" hits="1"/>
<line number="33" hits="1"/>
<line number="36" hits="1"/>
<line number="37" hits="1"/>
<line number="39" hits="1"/>
<line number="40" hits="1"/>
<line number="41" hits="1"/>
<line number="46" hits="1"/>
<line number="47" hits="1"/>
<line number="49" hits="1"/>
<line number="50" hits="1"/>
<line number="53" hits="1"/>
<line number="54" hits="1"/>
<line number="61" hits="1"/>
<line number="62" hits="1"/>
<line number="63" hits="1"/>
<line number="64" hits="1"/>
<line number="65" hits="1"/>
<line number="67" hits="1"/>
<line number="68" hits="1"/>
<line number="70" hits="0"/>
<line number="72" hits="1"/>
<line number="73" hits="1"/>
<line number="75" hits="0"/>
<line number="77" hits="1"/>
<line number="78" hits="1"/>
<line number="80" hits="0"/>
<line number="82" hits="1"/>
<line number="83" hits="1"/>
<line number="85" hits="0"/>
<line number="87" hits="1"/>
<line number="88" hits="1"/>
<line number="102" hits="0"/>
<line number="103" hits="0"/>
<line number="105" hits="0"/>
<line number="106" hits="0"/>
<line number="107" hits="0"/>
<line number="108" hits="0"/>
<line number="109" hits="0"/>
<line number="111" hits="0"/>
<line number="112" hits="0"/>
<line number="113" hits="0"/>
<line number="114" hits="0"/>
<line number="116" hits="0"/>
<line number="121" hits="0"/>
<line number="129" hits="1"/>
<line number="139" hits="0"/>
<line number="140" hits="0"/>
<line number="143" hits="1"/>
<line number="144" hits="0"/>
<line number="145" hits="0"/>
</lines>
</class>
<class name="metadata.py" filename="/work/kschmidt/web/orchestration/metadata.py" complexity="0" line-rate="0.3208" branch-rate="0">
<methods/>
<lines>
<line number="15" hits="1"/>
<line number="16" hits="1"/>
<line number="17" hits="1"/>
<line number="18" hits="1"/>
<line number="21" hits="1"/>
<line number="32" hits="0"/>
<line number="33" hits="0"/>
<line number="35" hits="0"/>
<line number="36" hits="0"/>
<line number="37" hits="0"/>
<line number="38" hits="0"/>
<line number="39" hits="0"/>
<line number="40" hits="0"/>
<line number="41" hits="0"/>
<line number="43" hits="0"/>
<line number="45" hits="0"/>
<line number="46" hits="0"/>
<line number="47" hits="0"/>
<line number="48" hits="0"/>
<line number="51" hits="1"/>
<line number="62" hits="1"/>
<line number="63" hits="1"/>
<line number="64" hits="1"/>
<line number="65" hits="0"/>
<line number="67" hits="1"/>
<line number="70" hits="1"/>
<line number="82" hits="0"/>
<line number="84" hits="0"/>
<line number="85" hits="0"/>
<line number="86" hits="0"/>
<line number="87" hits="0"/>
<line number="90" hits="0"/>
<line number="93" hits="1"/>
<line number="108" hits="1"/>
<line number="109" hits="1"/>
<line number="110" hits="1"/>
<line number="113" hits="1"/>
<line number="128" hits="0"/>
<line number="129" hits="0"/>
<line number="132" hits="0"/>
<line number="133" hits="0"/>
<line number="134" hits="0"/>
<line number="135" hits="0"/>
<line number="136" hits="0"/>
<line number="139" hits="0"/>
<line number="142" hits="1"/>
<line number="153" hits="0"/>
<line number="154" hits="0"/>
<line number="155" hits="0"/>
<line number="156" hits="0"/>
<line number="157" hits="0"/>
<line number="158" hits="0"/>
<line number="159" hits="0"/>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>
+17 -20
View File
@@ -15,15 +15,14 @@ Features:
import subprocess
import shutil
import os
import sys
from pathlib import Path
from typing import Dict, Any, Optional
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
from orchestration.config import Config
from orchestration.metadata import (
from utils.config import Config
from utils.metadata import (
load_folder_metadata,
merge_metadata,
resolve_metadata_for_plot,
@@ -122,7 +121,7 @@ def build_gallery(source_dir: Path, web_dir: Path,
"""
if relative_path is None:
relative_path = Path(".")
if inherited_metadata is None:
inherited_metadata = {}
@@ -186,7 +185,7 @@ def build_gallery(source_dir: Path, web_dir: Path,
subdir_names.append(subdir.name)
output_html = web_dir / "index.html"
# Always regenerate HTML to ensure subdirectory changes are reflected
# This ensures new subdirectories appear in navigation
force_regeneration = True
@@ -336,7 +335,7 @@ def main(clean_first: bool = False) -> None:
# Always ensure assets are up to date
assets_src = Path("assets")
assets_dst = gallery_root.parent / "assets"
if assets_src.exists():
# Update assets if they don't exist or are outdated
main_css_src = assets_src / "css" / "main.css"
@@ -400,7 +399,7 @@ def main(clean_first: bool = False) -> None:
# Calculate relative path to assets for single file
# Single files are at depth 1 (gallery_root/source.name/index.html)
assets_path = "../assets"
output_html = source_web_dir / "index.html"
with output_html.open("w") as f:
f.write(template.render(
@@ -414,7 +413,8 @@ def main(clean_first: bool = False) -> None:
folder_metadata={},
assets_path=assets_path,
source_dir=str(source_path.parent),
metadata_file_path=get_metadata_file_path(source_path.parent)
metadata_file_path=get_metadata_file_path(
source_path.parent)
))
print(f"Generated {output_html}")
@@ -433,16 +433,13 @@ def main(clean_first: bool = False) -> None:
if __name__ == "__main__":
if 'GATEWAY_INTERFACE' in os.environ:
refresh_gallery_cgi()
else:
import argparse
import argparse
parser = argparse.ArgumentParser(description='Generate gallery')
parser.add_argument(
'--clean',
action='store_true',
help='Clean gallery directory before generation'
)
args = parser.parse_args()
main(clean_first=args.clean)
parser = argparse.ArgumentParser(description='Generate gallery')
parser.add_argument(
'--clean',
action='store_true',
help='Clean gallery directory before generation'
)
args = parser.parse_args()
main(clean_first=args.clean)
-92
View File
@@ -1,92 +0,0 @@
#!/usr/bin/env python3
"""
Gallery Creation Time Integration
Example function to add creation time metadata to plot items.
This should be integrated into your existing Python gallery generation code.
"""
from pathlib import Path
def add_creation_time_to_items(items, base_path):
"""
Add creation time to plot items for sorting functionality.
Args:
items: List of plot item dictionaries
base_path: Base path where plot files are located
Returns:
Updated items list with creation_time field
"""
for item in items:
try:
# Try to get creation time from PNG file first, then PDF
png_path = None
pdf_path = None
# Extract relative path from href
if 'png_href' in item:
png_rel_path = item['png_href'].replace('../', '').replace(
'./', '')
png_path = Path(base_path) / png_rel_path
if 'pdf_href' in item:
pdf_rel_path = item['pdf_href'].replace('../', '').replace(
'./', '')
pdf_path = Path(base_path) / pdf_rel_path
# Use PNG creation time if available, otherwise PDF
creation_time = 0
if png_path and png_path.exists():
creation_time = int(png_path.stat().st_ctime)
elif pdf_path and pdf_path.exists():
creation_time = int(pdf_path.stat().st_ctime)
# Add creation time as timestamp (JavaScript can handle this)
item['creation_time'] = creation_time
except Exception as e:
# Fallback to 0 if there's any error
name = item.get('name', 'unknown')
print(f"Warning: Could not get creation time for {name}: {e}")
item['creation_time'] = 0
return items
def example_integration():
"""
Example of how to integrate this into your existing gallery generation.
"""
# This would be part of your existing gallery generation code
items = [
{
'name': 'plot1.png',
'png_href': './plot1.png',
'pdf_href': './plot1.pdf'
},
{
'name': 'plot2.png',
'png_href': './plot2.png',
'pdf_href': './plot2.pdf'
}
]
base_path = "/path/to/your/gallery/directory"
# Add creation times
items_with_time = add_creation_time_to_items(items, base_path)
# Now items_with_time can be passed to your Jinja2 template
# The template will have access to item.creation_time for each item
return items_with_time
if __name__ == "__main__":
# Test the function
items = example_integration()
for item in items:
created = item['creation_time']
print(f"Plot: {item['name']}, Created: {created}")
-45
View File
@@ -1,45 +0,0 @@
from typing import Dict, Any
import json
from pathlib import Path
def open_metadata(path: str, filename: str = "metadata.json") -> Dict[str, Any]:
"""
Open metadata file and return its contents as a dictionary.
Args:
path: The directory path where the metadata file is located.
filename: The name of the metadata file (default: "metadata.json").
Returns:
A dictionary containing the metadata.
"""
with open(Path(path) / filename, 'r', encoding='utf-8') as f:
try:
return json.load(f)
except json.JSONDecodeError as e:
raise ValueError(f"Error decoding JSON from {filename}: {e}")
except FileNotFoundError:
raise FileNotFoundError(f"Metadata file {filename} not found in {path}")
except Exception as e:
raise RuntimeError(f"Unexpected error reading metadata: {e}")
def merge_metadata(
base_metadata: Dict[str, Any],
additional_metadata: Dict[str, Any]
) -> Dict[str, Any]:
"""
Merge two metadata dictionaries.
Args:
base_metadata: The base metadata dictionary.
additional_metadata: The additional metadata dictionary to merge.
Returns:
A new dictionary containing the merged metadata.
"""
merged = base_metadata.copy()
for key, value in additional_metadata.items():
merged[key] = value
return merged
BIN
View File
Binary file not shown.
-31
View File
@@ -1,31 +0,0 @@
#!/bin/bash
set -e
echo "🧹 Cleaning up test directory..."
echo
# Remove Python cache files
echo "Removing Python cache files..."
find /work/kschmidt/web -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
find /work/kschmidt/web -name "*.pyc" -delete 2>/dev/null || true
# Remove temporary test files
echo "Removing temporary test files..."
rm -f /work/kschmidt/web/tests/*.sif
rm -f /work/kschmidt/web/tests/test-results.xml
rm -f /work/kschmidt/web/*.sif
rm -f /work/kschmidt/web/.coverage
rm -rf /work/kschmidt/web/coverage_html_report/
rm -f /work/kschmidt/web/coverage.xml
# List remaining test files
echo
echo "📁 Remaining test files:"
ls -la /work/kschmidt/web/tests/
echo
echo "📊 Test directory size:"
du -sh /work/kschmidt/web/tests/
echo
echo "✅ Test directory cleanup complete!"
-5277
View File
File diff suppressed because it is too large Load Diff
-113
View File
@@ -1,113 +0,0 @@
#!/bin/bash
"""
Automated coverage testing script for the gallery generator.
This script runs tests with coverage analysis and generates comprehensive reports.
"""
import subprocess
import sys
import os
from pathlib import Path
def run_coverage_tests():
"""Run tests with coverage analysis."""
print("🔬 Starting automated coverage testing...")
# Ensure we're in the right directory
os.chdir('/src' if Path('/src').exists() else Path(__file__).parent.parent)
# Remove old coverage data
subprocess.run(['coverage', 'erase'], capture_output=True)
# Run tests with coverage
print("📊 Running tests with coverage analysis...")
test_files = [
'tests/test_simple_coverage.py',
'tests/test_container.py',
'tests/test_build_container.py'
]
success = True
for test_file in test_files:
if Path(test_file).exists():
print(f" Running {test_file}...")
result = subprocess.run([
'coverage', 'run', '--append', '-m', 'unittest',
test_file.replace('/', '.').replace('.py', '')
], capture_output=True, text=True)
if result.returncode != 0:
print(f"❌ Failed: {test_file}")
print(f"Error: {result.stderr}")
success = False
else:
print(f"✅ Passed: {test_file}")
if not success:
print("❌ Some tests failed. Coverage report may be incomplete.")
return False
# Generate coverage reports
print("\n📈 Generating coverage reports...")
# Console report
print("\n🖥️ Console Coverage Report:")
subprocess.run(['coverage', 'report'])
# HTML report
html_result = subprocess.run(['coverage', 'html'], capture_output=True, text=True)
if html_result.returncode == 0:
print("\n🌐 HTML coverage report generated: coverage_html_report/index.html")
# XML report for CI/CD
xml_result = subprocess.run(['coverage', 'xml'], capture_output=True, text=True)
if xml_result.returncode == 0:
print("📄 XML coverage report generated: coverage.xml")
# Coverage percentage
percentage_result = subprocess.run([
'coverage', 'report', '--format=total'
], capture_output=True, text=True)
if percentage_result.returncode == 0:
try:
coverage_pct = float(percentage_result.stdout.strip())
print(f"\n🎯 Total Coverage: {coverage_pct:.2f}%")
if coverage_pct >= 80:
print("✅ Coverage target met (≥80%)")
return True
else:
print("⚠️ Coverage below target (≥80%)")
return False
except ValueError:
print("⚠️ Could not parse coverage percentage")
return success
def main():
"""Main coverage testing function."""
print("=" * 60)
print("🧪 Gallery Generator - Automated Coverage Testing")
print("=" * 60)
try:
success = run_coverage_tests()
if success:
print("\n✅ Coverage testing completed successfully!")
sys.exit(0)
else:
print("\n❌ Coverage testing failed!")
sys.exit(1)
except Exception as e:
print(f"\n💥 Coverage testing error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
-50
View File
@@ -1,50 +0,0 @@
#!/bin/bash
# Local coverage testing script - run coverage without container
echo "🔬 Running local coverage testing..."
# Ensure coverage is installed
pip install coverage 2>/dev/null || echo "Coverage already installed"
# Change to project root
cd /work/kschmidt/web
# Clean previous coverage data
coverage erase
# Run tests with coverage
echo "📊 Running tests with coverage..."
coverage run --source=/work/kschmidt/web /work/kschmidt/web/tests/test_simple_coverage.py 2>/dev/null || \
coverage run --append --source=/work/kschmidt/web -m unittest tests.test_container 2>/dev/null || \
coverage run --append --source=/work/kschmidt/web -m unittest tests.test_build_container 2>/dev/null || \
echo "Running fallback coverage..."
# Generate reports
echo "📈 Generating coverage reports..."
echo
echo "🖥️ Console Coverage Report:"
coverage report --include="*generate_gallery*,*orchestration*" || coverage report
echo
echo "🌐 Generating HTML report..."
coverage html --directory=coverage_html_report
echo "HTML report generated: coverage_html_report/index.html"
echo
echo "📄 Generating XML report..."
coverage xml
echo "XML report generated: coverage.xml"
# Show coverage percentage
COVERAGE_PCT=$(coverage report --format=total 2>/dev/null || echo "0")
echo
echo "🎯 Total Coverage: ${COVERAGE_PCT}%"
if [ "${COVERAGE_PCT}" != "0" ] && (( $(echo "$COVERAGE_PCT >= 80" | bc -l 2>/dev/null || echo "0") )); then
echo "✅ Coverage target met (≥80%)"
else
echo "⚠️ Coverage below target (≥80%)"
fi
echo
echo "✅ Local coverage testing complete!"
-78
View File
@@ -1,78 +0,0 @@
#!/usr/bin/env python3
"""
Clean pytest-based coverage runner for container environment.
"""
import subprocess
import sys
import os
from pathlib import Path
def run_pytest_with_coverage():
"""Run pytest with coverage analysis."""
print("🧪 Running pytest with coverage...")
# Ensure we're in the right directory
if Path('/src').exists():
os.chdir('/src')
else:
os.chdir(Path(__file__).parent.parent)
# Clean previous coverage data
subprocess.run(['coverage', 'erase'], capture_output=True)
# Run pytest with coverage
cmd = [
'python3', '-m', 'pytest',
'tests/test_pytest_suite.py',
'--cov=.',
'--cov-report=term-missing',
'--cov-report=html:coverage_html_report',
'--cov-report=xml:coverage.xml',
'--cov-config=.coveragerc',
'-v'
]
print(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd)
if result.returncode == 0:
print("\n✅ Pytest coverage completed successfully!")
# Extract coverage percentage
try:
coverage_result = subprocess.run(
['coverage', 'report', '--format=total'],
capture_output=True, text=True
)
if coverage_result.returncode == 0:
coverage_pct = float(coverage_result.stdout.strip())
print(f"🎯 Total Coverage: {coverage_pct:.2f}%")
if coverage_pct >= 80:
print("✅ Coverage target met (≥80%)")
return True
else:
print("⚠️ Coverage below target (≥80%)")
except (ValueError, subprocess.SubprocessError):
print("⚠️ Could not extract coverage percentage")
return True
else:
print("❌ Pytest coverage failed!")
return False
def main():
"""Main entry point."""
print("=" * 60)
print("🔬 Gallery Generator - Pytest Coverage Testing")
print("=" * 60)
success = run_pytest_with_coverage()
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
-213
View File
@@ -1,213 +0,0 @@
"""
Test for building and validating the Apptainer container.
"""
import unittest
import subprocess
import os
import tempfile
import shutil
from pathlib import Path
class TestContainerBuild(unittest.TestCase):
"""Test Apptainer container building and basic functionality."""
@classmethod
def setUpClass(cls):
"""Set up test environment - run once for all tests."""
cls.project_root = Path(__file__).parent.parent.absolute()
cls.singularity_def = cls.project_root / "Singularity.def"
cls.test_dir = Path(tempfile.mkdtemp())
cls.container_path = cls.test_dir / "gallery_test.sif"
print(f"Project root: {cls.project_root}")
print(f"Test directory: {cls.test_dir}")
@classmethod
def tearDownClass(cls):
"""Clean up test environment."""
if cls.test_dir.exists():
shutil.rmtree(cls.test_dir)
def test_01_singularity_def_exists(self):
"""Test that Singularity.def file exists and is valid."""
self.assertTrue(self.singularity_def.exists(),
"Singularity.def file not found")
content = self.singularity_def.read_text()
self.assertIn("Bootstrap:", content)
self.assertIn("From:", content)
self.assertIn("%post", content)
self.assertIn("imagemagick", content.lower())
self.assertIn("jinja2", content.lower())
self.assertIn("pyyaml", content.lower())
def test_02_apptainer_available(self):
"""Test that Apptainer/Singularity is available."""
try:
# Try apptainer first (newer)
result = subprocess.run(['apptainer', '--version'],
capture_output=True, text=True, timeout=10)
if result.returncode == 0:
self.container_cmd = 'apptainer'
return
except FileNotFoundError:
pass
try:
# Fall back to singularity
result = subprocess.run(['singularity', '--version'],
capture_output=True, text=True, timeout=10)
if result.returncode == 0:
self.container_cmd = 'singularity'
return
except FileNotFoundError:
pass
self.fail("Neither 'apptainer' nor 'singularity' command found")
def test_03_build_container(self):
"""Test building the container from Singularity.def."""
# Ensure we have a container command from previous test
if not hasattr(self, 'container_cmd'):
self.test_02_apptainer_available()
print(f"Building container with {self.container_cmd}...")
# Build command
build_cmd = [
self.container_cmd, 'build',
str(self.container_path),
str(self.singularity_def)
]
# Change to project directory for build context
original_cwd = os.getcwd()
try:
os.chdir(self.project_root)
# Run build with extended timeout
result = subprocess.run(
build_cmd,
capture_output=True,
text=True,
timeout=300 # 5 minutes should be enough
)
if result.returncode != 0:
print("STDOUT:", result.stdout)
print("STDERR:", result.stderr)
self.fail(f"Container build failed with return code {result.returncode}")
# Verify container was created
self.assertTrue(self.container_path.exists(),
"Container file was not created")
# Check container size (should be > 100MB for a real container)
size_mb = self.container_path.stat().st_size / (1024 * 1024)
self.assertGreater(size_mb, 50,
f"Container seems too small: {size_mb:.1f}MB")
print(f"✅ Container built successfully: {size_mb:.1f}MB")
finally:
os.chdir(original_cwd)
def test_04_container_exec_python(self):
"""Test that Python works inside the container."""
if not self.container_path.exists():
self.skipTest("Container not built")
cmd = [self.container_cmd, 'exec', str(self.container_path),
'python3', '-c', 'import sys; print(f"Python {sys.version_info.major}.{sys.version_info.minor}")']
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
self.assertEqual(result.returncode, 0,
f"Python execution failed: {result.stderr}")
self.assertIn("Python 3.", result.stdout)
def test_05_container_dependencies(self):
"""Test that required dependencies are installed."""
if not self.container_path.exists():
self.skipTest("Container not built")
# Test Python dependencies
python_test = """
import sys
try:
import jinja2
import yaml
print("✅ Python dependencies OK")
except ImportError as e:
print(f"❌ Missing dependency: {e}")
sys.exit(1)
"""
cmd = [self.container_cmd, 'exec', str(self.container_path),
'python3', '-c', python_test]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
self.assertEqual(result.returncode, 0,
f"Dependency check failed: {result.stderr}")
self.assertIn("Python dependencies OK", result.stdout)
# Test ImageMagick
cmd = [self.container_cmd, 'exec', str(self.container_path),
'convert', '-version']
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
self.assertEqual(result.returncode, 0,
f"ImageMagick not working: {result.stderr}")
self.assertIn("ImageMagick", result.stdout)
def test_06_container_run_tests(self):
"""Test running the container test suite."""
if not self.container_path.exists():
self.skipTest("Container not built")
# Run the container tests
cmd = [self.container_cmd, 'exec', str(self.container_path),
'python3', '/src/tests/test_container.py']
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
print("Container test output:")
print(result.stdout)
if result.stderr:
print("Errors:")
print(result.stderr)
self.assertEqual(result.returncode, 0,
f"Container tests failed: {result.stderr}")
self.assertIn("ALL TESTS PASSED", result.stdout)
def run_build_tests():
"""Run container build tests."""
print("=" * 60)
print("APPTAINER CONTAINER BUILD TESTS")
print("=" * 60)
# Run tests in order
suite = unittest.TestLoader().loadTestsFromTestCase(TestContainerBuild)
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
if result.wasSuccessful():
print("\n✅ All container build tests passed!")
else:
print(f"\n❌ Container build tests failed!")
print(f"Failures: {len(result.failures)}")
print(f"Errors: {len(result.errors)}")
return 0 if result.wasSuccessful() else 1
if __name__ == '__main__':
import sys
exit_code = run_build_tests()
sys.exit(exit_code)
-250
View File
@@ -1,250 +0,0 @@
"""
Container-optimized test suite for gallery generator.
Designed to run inside Apptainer/Singularity containers.
"""
import unittest
import tempfile
import shutil
import time
import os
from pathlib import Path
import subprocess
import sys
class TestContainerEnvironment(unittest.TestCase):
"""Test that the container environment is properly configured."""
def test_python_version(self):
"""Test that Python 3.11+ is available."""
version = sys.version_info
self.assertGreaterEqual(version.major, 3)
self.assertGreaterEqual(version.minor, 11)
def test_required_modules(self):
"""Test that required Python modules are installed."""
try:
import jinja2
import yaml
self.assertTrue(True) # Success if no ImportError
except ImportError as e:
self.fail(f"Required module not found: {e}")
def test_imagemagick_available(self):
"""Test that ImageMagick is installed and accessible."""
try:
result = subprocess.run(['convert', '-version'],
capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0)
self.assertIn('ImageMagick', result.stdout)
except (subprocess.TimeoutExpired, FileNotFoundError):
self.fail("ImageMagick not available or not working")
def test_working_directory(self):
"""Test that the source code is available."""
expected_files = ['generate_gallery.py', 'config.yaml', 'orchestration/']
for file_path in expected_files:
path = Path('/src') / file_path
self.assertTrue(path.exists(), f"Missing: {file_path}")
class TestUtilityFunctions(unittest.TestCase):
"""Test core utility functions."""
def test_format_file_size(self):
"""Test file size formatting utility."""
# Import the function from the container's source
sys.path.insert(0, '/src')
from generate_gallery import format_file_size
self.assertEqual(format_file_size(0), "0 B")
self.assertEqual(format_file_size(1024), "1.0 KB")
self.assertEqual(format_file_size(1048576), "1.0 MB")
self.assertEqual(format_file_size(1073741824), "1.0 GB")
def test_needs_update(self):
"""Test file update checking."""
sys.path.insert(0, '/src')
from generate_gallery import needs_update
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Test missing target
source = temp_path / "source.txt"
target = temp_path / "target.txt"
source.write_text("test")
self.assertTrue(needs_update(source, target))
# Test up-to-date target
target.write_text("test")
time.sleep(0.1) # Ensure different timestamp
os.utime(target, (time.time(), time.time()))
self.assertFalse(needs_update(source, target))
class TestMetadataSystem(unittest.TestCase):
"""Test metadata loading and processing."""
def setUp(self):
sys.path.insert(0, '/src')
def test_metadata_loading(self):
"""Test loading metadata files."""
from orchestration.metadata import load_metadata_file
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Test YAML metadata
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)
self.assertEqual(metadata['title'], 'Test')
self.assertEqual(metadata['author'], 'Container Test')
def test_metadata_inheritance(self):
"""Test metadata inheritance through directories."""
from orchestration.metadata import merge_metadata
parent = {'project': 'Test', 'version': '1.0'}
child = {'experiment': 'A', 'version': '1.1'}
merged = merge_metadata(parent, child)
self.assertEqual(merged['project'], 'Test')
self.assertEqual(merged['experiment'], 'A')
self.assertEqual(merged['version'], '1.1') # Child overrides parent
class TestPDFProcessing(unittest.TestCase):
"""Test PDF processing functionality."""
def setUp(self):
sys.path.insert(0, '/src')
self.temp_dir = Path(tempfile.mkdtemp())
def tearDown(self):
shutil.rmtree(self.temp_dir)
def create_mock_pdf(self, path: Path):
"""Create a minimal mock PDF."""
path.write_text("%PDF-1.4\nMock PDF for testing")
def test_pdf_conversion(self):
"""Test PDF to PNG conversion."""
from generate_gallery import convert_pdf_to_png
# Create a mock PDF
pdf_path = self.temp_dir / "test.pdf"
self.create_mock_pdf(pdf_path)
# This should work in the container with ImageMagick
try:
convert_pdf_to_png(pdf_path)
png_path = pdf_path.with_suffix('.png')
self.assertTrue(png_path.exists())
except subprocess.CalledProcessError:
# Allow test to pass if ImageMagick can't process our mock PDF
# (Real PDFs would work, but our mock might not)
self.skipTest("Mock PDF not processable by ImageMagick")
class TestGalleryGeneration(unittest.TestCase):
"""Test end-to-end gallery generation."""
def setUp(self):
sys.path.insert(0, '/src')
self.temp_dir = Path(tempfile.mkdtemp())
self.source_dir = self.temp_dir / "source"
self.web_dir = self.temp_dir / "web"
self.source_dir.mkdir()
self.web_dir.mkdir()
def tearDown(self):
shutil.rmtree(self.temp_dir)
def create_test_structure(self):
"""Create a minimal test directory structure."""
# Create mock PDF
pdf_path = self.source_dir / "test_plot.pdf"
pdf_path.write_text("%PDF-1.4\nTest plot content")
# Create metadata
metadata_path = self.source_dir / "metadata.yaml"
metadata_path.write_text("title: Container Test\nauthor: CI Pipeline\n")
def test_build_gallery(self):
"""Test building a simple gallery."""
from generate_gallery import build_gallery
self.create_test_structure()
# This should complete without errors
try:
build_gallery(self.source_dir, self.web_dir)
# Check that HTML was generated
html_file = self.web_dir / "index.html"
self.assertTrue(html_file.exists())
# Check that files were copied
pdf_file = self.web_dir / "test_plot.pdf"
self.assertTrue(pdf_file.exists())
except Exception as e:
self.fail(f"Gallery generation failed: {e}")
def run_container_tests():
"""Run all tests suitable for container execution."""
print("=" * 60)
print("GALLERY GENERATOR CONTAINER TESTS")
print("=" * 60)
print(f"Python version: {sys.version}")
print(f"Working directory: {os.getcwd()}")
print(f"Python path: {sys.path[:3]}...")
print("=" * 60)
# Create test suite
loader = unittest.TestLoader()
suite = unittest.TestSuite()
# Add test classes
test_classes = [
TestContainerEnvironment,
TestUtilityFunctions,
TestMetadataSystem,
TestPDFProcessing,
TestGalleryGeneration
]
for test_class in test_classes:
tests = loader.loadTestsFromTestCase(test_class)
suite.addTests(tests)
# Run tests
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
# Print summary
print("=" * 60)
if result.wasSuccessful():
print("✅ ALL TESTS PASSED")
else:
print("❌ SOME TESTS FAILED")
print(f"Failures: {len(result.failures)}")
print(f"Errors: {len(result.errors)}")
print("=" * 60)
return 0 if result.wasSuccessful() else 1
if __name__ == '__main__':
exit_code = run_container_tests()
sys.exit(exit_code)
-104
View File
@@ -1,104 +0,0 @@
#!/bin/bash
set -e
echo "==================================================================="
echo "Gallery Generator Container Test Suite with Coverage"
echo "==================================================================="
echo
# Check if we're in the right directory
if [[ ! -f "Singularity.def" ]]; then
echo "❌ Error: Singularity.def not found. Please run from project root."
exit 1
fi
# Check for apptainer/singularity
CONTAINER_CMD=""
if command -v apptainer &> /dev/null; then
CONTAINER_CMD="apptainer"
elif command -v singularity &> /dev/null; then
CONTAINER_CMD="singularity"
else
echo "❌ Error: Neither 'apptainer' nor 'singularity' found."
echo "Please install Apptainer/Singularity to run container tests."
exit 1
fi
echo "Using container runtime: $CONTAINER_CMD"
echo
# Container file
CONTAINER_FILE="gallery-test.sif"
# Clean up any existing container
if [[ -f "$CONTAINER_FILE" ]]; then
echo "🧹 Removing existing container..."
rm -f "$CONTAINER_FILE"
fi
echo "🔨 Building container..."
echo "Command: $CONTAINER_CMD build $CONTAINER_FILE Singularity.def"
echo
if ! $CONTAINER_CMD build "$CONTAINER_FILE" Singularity.def; then
echo "❌ Container build failed!"
exit 1
fi
echo
echo "✅ Container built successfully!"
echo "Container size: $(du -h "$CONTAINER_FILE" | cut -f1)"
echo
echo "🧪 Running container build tests..."
echo
if ! python3 tests/test_build_container.py; then
echo "❌ Container build tests failed!"
exit 1
fi
echo
echo "🧪 Running tests inside container..."
echo
if ! $CONTAINER_CMD exec "$CONTAINER_FILE" python3 /src/tests/test_container.py; then
echo "❌ Container tests failed!"
exit 1
fi
echo
echo "🧪 Testing container runtime..."
echo
echo "Python version in container:"
$CONTAINER_CMD exec "$CONTAINER_FILE" python3 --version
echo
echo "Installed packages:"
$CONTAINER_CMD exec "$CONTAINER_FILE" pip list
echo
echo "Testing ImageMagick:"
$CONTAINER_CMD exec "$CONTAINER_FILE" convert -version | head -n 2
echo
echo "🧪 Running automated coverage tests..."
$CONTAINER_CMD exec "$CONTAINER_FILE" python3 /src/tests/run_coverage.py
echo
echo "==================================================================="
echo "✅ ALL TESTS PASSED!"
echo "Container is ready for use with coverage analysis completed."
echo "==================================================================="
echo
echo "To use the container:"
echo " $CONTAINER_CMD run $CONTAINER_FILE [args]"
echo " $CONTAINER_CMD exec $CONTAINER_FILE python3 /src/generate_gallery.py [args]"
echo " $CONTAINER_CMD exec $CONTAINER_FILE python3 /src/tests/run_coverage.py # Run coverage tests"
echo
# Optional: Clean up
read -p "Remove test container? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -f "$CONTAINER_FILE"
echo "🧹 Test container removed."
fi
-258
View File
@@ -1,258 +0,0 @@
"""
Test coverage analysis for the gallery generator container test suite.
This module analyzes what functionality is covered by our streamlined tests.
"""
import unittest
import inspect
import sys
from pathlib import Path
class TestCoverage(unittest.TestCase):
"""Analyze test coverage of the container test suite."""
def setUp(self):
"""Set up test environment."""
sys.path.insert(0, '/src' if Path('/src').exists() else str(Path(__file__).parent.parent))
def test_core_functions_covered(self):
"""Test that core functions are covered by our test suite."""
try:
from generate_gallery import (
format_file_size,
needs_update,
convert_pdf_to_png,
build_gallery,
calculate_directory_stats
)
# These functions should be importable
self.assertTrue(callable(format_file_size))
self.assertTrue(callable(needs_update))
self.assertTrue(callable(convert_pdf_to_png))
self.assertTrue(callable(build_gallery))
self.assertTrue(callable(calculate_directory_stats))
print("✅ Core functions are accessible")
except ImportError as e:
self.fail(f"Core functions not accessible: {e}")
def test_metadata_functions_covered(self):
"""Test that metadata functions are covered."""
try:
from orchestration.metadata import (
load_metadata_file,
load_folder_metadata,
merge_metadata,
resolve_metadata_for_plot,
save_metadata_cache
)
# These functions should be importable
self.assertTrue(callable(load_metadata_file))
self.assertTrue(callable(load_folder_metadata))
self.assertTrue(callable(merge_metadata))
self.assertTrue(callable(resolve_metadata_for_plot))
self.assertTrue(callable(save_metadata_cache))
print("✅ Metadata functions are accessible")
except ImportError as e:
self.fail(f"Metadata functions not accessible: {e}")
def test_config_functions_covered(self):
"""Test that config functions are covered."""
try:
from orchestration.config import Config
self.assertTrue(hasattr(Config, 'from_yaml'))
print("✅ Config functions are accessible")
except ImportError as e:
self.fail(f"Config functions not accessible: {e}")
def test_logger_functions_covered(self):
"""Test that logger functions are covered."""
try:
from orchestration.logger import GalleryLogger, create_logger
self.assertTrue(callable(GalleryLogger))
self.assertTrue(callable(create_logger))
print("✅ Logger functions are accessible")
except ImportError as e:
self.fail(f"Logger functions not accessible: {e}")
def test_container_test_completeness(self):
"""Analyze what our container tests actually cover."""
from test_container import (
TestContainerEnvironment,
TestUtilityFunctions,
TestMetadataSystem,
TestPDFProcessing,
TestGalleryGeneration
)
# Count test methods in each class
coverage_map = {}
test_classes = [
TestContainerEnvironment,
TestUtilityFunctions,
TestMetadataSystem,
TestPDFProcessing,
TestGalleryGeneration
]
total_tests = 0
for test_class in test_classes:
methods = [m for m in dir(test_class) if m.startswith('test_')]
coverage_map[test_class.__name__] = len(methods)
total_tests += len(methods)
print(f"\n📊 Container Test Coverage Analysis:")
print(f" Total test methods: {total_tests}")
for class_name, count in coverage_map.items():
print(f" {class_name}: {count} tests")
# Ensure we have comprehensive coverage
self.assertGreaterEqual(total_tests, 8, "Should have at least 8 test methods")
self.assertGreater(coverage_map['TestContainerEnvironment'], 2,
"Should test container environment thoroughly")
self.assertGreater(coverage_map['TestUtilityFunctions'], 1,
"Should test utility functions")
self.assertGreater(coverage_map['TestMetadataSystem'], 1,
"Should test metadata system")
def test_critical_paths_covered(self):
"""Test that critical execution paths are covered."""
critical_paths = {
'PDF conversion': 'convert_pdf_to_png',
'Gallery building': 'build_gallery',
'Metadata loading': 'load_metadata_file',
'File operations': 'needs_update',
'Configuration': 'Config.from_yaml'
}
print(f"\n🎯 Critical Path Coverage:")
covered_paths = []
for path_name, function_name in critical_paths.items():
try:
if '.' in function_name:
# Handle class methods
module_name, method_name = function_name.split('.')
if module_name == 'Config':
from orchestration.config import Config
self.assertTrue(hasattr(Config, method_name))
else:
# Handle regular functions
if function_name in ['convert_pdf_to_png', 'build_gallery', 'needs_update']:
from generate_gallery import convert_pdf_to_png, build_gallery, needs_update
elif function_name == 'load_metadata_file':
from orchestration.metadata import load_metadata_file
covered_paths.append(path_name)
print(f"{path_name}")
except ImportError:
print(f"{path_name} - not accessible")
coverage_percentage = (len(covered_paths) / len(critical_paths)) * 100
print(f"\n📈 Critical path coverage: {coverage_percentage:.1f}%")
self.assertGreaterEqual(coverage_percentage, 80,
"Should cover at least 80% of critical paths")
def test_dependency_coverage(self):
"""Test that all required dependencies are covered."""
required_deps = ['jinja2', 'yaml', 'subprocess', 'pathlib']
print(f"\n🔗 Dependency Coverage:")
covered_deps = []
for dep in required_deps:
try:
if dep == 'yaml':
import yaml
elif dep == 'jinja2':
import jinja2
elif dep == 'subprocess':
import subprocess
elif dep == 'pathlib':
import pathlib
covered_deps.append(dep)
print(f"{dep}")
except ImportError:
print(f"{dep} - not available")
coverage_percentage = (len(covered_deps) / len(required_deps)) * 100
print(f"\n📈 Dependency coverage: {coverage_percentage:.1f}%")
self.assertGreaterEqual(coverage_percentage, 75,
"Should have at least 75% of dependencies available")
def analyze_test_coverage():
"""Run coverage analysis and print detailed report."""
print("=" * 60)
print("GALLERY GENERATOR TEST COVERAGE ANALYSIS")
print("=" * 60)
# Run coverage tests
loader = unittest.TestLoader()
suite = loader.loadTestsFromTestCase(TestCoverage)
runner = unittest.TextTestRunner(verbosity=2, stream=sys.stdout)
result = runner.run(suite)
print("\n" + "=" * 60)
print("COVERAGE SUMMARY")
print("=" * 60)
if result.wasSuccessful():
print("✅ All coverage requirements met!")
print("\n📋 Test Suite Status:")
print(" • Container environment validation: ✅")
print(" • Core functionality testing: ✅")
print(" • Metadata system testing: ✅")
print(" • PDF processing testing: ✅")
print(" • End-to-end workflow testing: ✅")
print(" • Dependency validation: ✅")
print("\n🎯 What our tests cover:")
print(" • Python 3.11+ environment")
print(" • jinja2 and pyyaml dependencies")
print(" • ImageMagick integration")
print(" • File operations and utilities")
print(" • YAML/JSON metadata processing")
print(" • PDF to PNG conversion")
print(" • Gallery generation workflow")
print(" • Error handling and edge cases")
print("\n✨ Benefits of our streamlined approach:")
print(" • No external test dependencies")
print(" • Container-native testing")
print(" • Real environment validation")
print(" • CI/CD pipeline integration")
print(" • Production-ready validation")
else:
print("❌ Some coverage requirements not met")
print(f" Failures: {len(result.failures)}")
print(f" Errors: {len(result.errors)}")
print("=" * 60)
return 0 if result.wasSuccessful() else 1
if __name__ == '__main__':
import sys
exit_code = analyze_test_coverage()
sys.exit(exit_code)
-134
View File
@@ -1,134 +0,0 @@
"""
Focused coverage test - tests actual functions without container dependencies
"""
import unittest
import tempfile
import os
import sys
from pathlib import Path
# Add project root to path
sys.path.insert(0, str(Path(__file__).parent.parent))
class TestActualFunctions(unittest.TestCase):
"""Test actual functions for coverage analysis."""
def setUp(self):
"""Set up test environment."""
self.test_dir = tempfile.mkdtemp()
def tearDown(self):
"""Clean up test environment."""
import shutil
shutil.rmtree(self.test_dir, ignore_errors=True)
def test_config_loading(self):
"""Test configuration loading."""
from orchestration.config import Config
# Create a test config file
config_content = """
title: "Test Gallery"
description: "Test gallery for coverage"
output_dir: "output"
"""
config_file = os.path.join(self.test_dir, "test_config.yaml")
with open(config_file, 'w') as f:
f.write(config_content)
# Test loading
config = Config.from_yaml(config_file)
self.assertEqual(config.title, "Test Gallery")
self.assertEqual(config.description, "Test gallery for coverage")
def test_metadata_functions(self):
"""Test metadata functions."""
from orchestration.metadata import load_metadata_file, merge_metadata
# Create test metadata
metadata_content = """
title: "Test Plot"
author: "Test Author"
date: "2025-01-01"
"""
metadata_file = os.path.join(self.test_dir, "metadata.yaml")
with open(metadata_file, 'w') as f:
f.write(metadata_content)
# Test loading
metadata = load_metadata_file(metadata_file)
self.assertEqual(metadata['title'], "Test Plot")
self.assertEqual(metadata['author'], "Test Author")
# Test merging
base_meta = {'title': 'Base', 'type': 'plot'}
override_meta = {'title': 'Override', 'new_field': 'value'}
merged = merge_metadata(base_meta, override_meta)
self.assertEqual(merged['title'], 'Override') # Override wins
self.assertEqual(merged['type'], 'plot') # Base preserved
self.assertEqual(merged['new_field'], 'value') # New field added
def test_logger_creation(self):
"""Test logger creation."""
from orchestration.logger import create_logger, GalleryLogger
# Test creating a logger
logger = create_logger("test_logger")
self.assertIsNotNone(logger)
# Test GalleryLogger
gallery_logger = GalleryLogger("test_gallery")
self.assertIsNotNone(gallery_logger)
def test_file_operations(self):
"""Test file operation utilities."""
# Create test files
old_file = os.path.join(self.test_dir, "old.txt")
new_file = os.path.join(self.test_dir, "new.txt")
# Create old file first
with open(old_file, 'w') as f:
f.write("old content")
# Wait a moment then create new file
import time
time.sleep(0.1)
with open(new_file, 'w') as f:
f.write("new content")
# Test file modification times
old_stat = os.stat(old_file)
new_stat = os.stat(new_file)
self.assertLess(old_stat.st_mtime, new_stat.st_mtime)
def test_path_operations(self):
"""Test path and directory operations."""
test_path = Path(self.test_dir)
# Test path exists
self.assertTrue(test_path.exists())
self.assertTrue(test_path.is_dir())
# Create subdirectory
subdir = test_path / "subdir"
subdir.mkdir()
self.assertTrue(subdir.exists())
self.assertTrue(subdir.is_dir())
# Create file in subdir
test_file = subdir / "test.txt"
test_file.write_text("test content")
self.assertTrue(test_file.exists())
self.assertTrue(test_file.is_file())
self.assertEqual(test_file.read_text(), "test content")
if __name__ == '__main__':
unittest.main()
-210
View File
@@ -1,210 +0,0 @@
"""
Pytest-based test suite for the gallery generator container.
Clean, focused tests using pytest conventions.
"""
import pytest
import sys
import tempfile
import shutil
from pathlib import Path
# Add project root to Python path for container testing
if Path('/src').exists():
sys.path.insert(0, '/src')
else:
sys.path.insert(0, str(Path(__file__).parent.parent))
@pytest.fixture
def temp_dir():
"""Create a temporary directory for tests."""
temp_path = tempfile.mkdtemp()
yield temp_path
shutil.rmtree(temp_path, ignore_errors=True)
@pytest.fixture
def mock_pdf_content():
"""Mock PDF content for testing."""
return (b'%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>'
b'\nendobj\nxref\n0 3\n0000000000 65535 f \ntrailer\n<<\n'
b'/Size 3\n/Root 1 0 R\n>>\nstartxref\n9\n%%EOF')
class TestEnvironment:
"""Test container environment and dependencies."""
def test_python_version(self):
"""Test Python version is correct."""
assert sys.version_info.major == 3
assert sys.version_info.minor >= 10
def test_required_packages(self):
"""Test required packages are available."""
import jinja2
import yaml
import coverage
assert jinja2.__version__
assert yaml.__version__
assert coverage.__version__
class TestCoreModules:
"""Test core application modules."""
def test_generate_gallery_import(self):
"""Test main module imports correctly."""
try:
import generate_gallery
assert hasattr(generate_gallery, 'main')
except ImportError:
pytest.skip("generate_gallery not available in test environment")
def test_config_module(self):
"""Test config module functionality."""
try:
from orchestration.config import Config
assert hasattr(Config, 'from_yaml')
except ImportError:
pytest.skip("Config module not available")
def test_logger_module(self):
"""Test logger module functionality."""
try:
from orchestration.logger import create_logger
logger = create_logger('test')
assert logger.name == 'test'
except ImportError:
pytest.skip("Logger module not available")
class TestUtilityFunctions:
"""Test utility functions."""
def test_file_operations(self, temp_dir):
"""Test basic file operations."""
test_file = Path(temp_dir) / 'test.txt'
test_file.write_text('test content')
assert test_file.exists()
assert test_file.read_text() == 'test content'
def test_directory_operations(self, temp_dir):
"""Test directory operations."""
test_subdir = Path(temp_dir) / 'subdir'
test_subdir.mkdir()
assert test_subdir.is_dir()
class TestPDFProcessing:
"""Test PDF-related functionality."""
def test_mock_pdf_creation(self, temp_dir, mock_pdf_content):
"""Test creating mock PDF files."""
pdf_path = Path(temp_dir) / 'test.pdf'
pdf_path.write_bytes(mock_pdf_content)
assert pdf_path.exists()
assert pdf_path.stat().st_size > 0
def test_imagemagick_available(self):
"""Test ImageMagick is available in container."""
import subprocess
try:
result = subprocess.run(['convert', '-version'],
capture_output=True, text=True)
assert result.returncode == 0
assert 'ImageMagick' in result.stdout
except FileNotFoundError:
pytest.skip("ImageMagick not available")
class TestMetadataSystem:
"""Test metadata handling."""
def test_yaml_processing(self, temp_dir):
"""Test YAML metadata processing."""
import yaml
metadata = {
'title': 'Test Gallery',
'description': 'Test description',
'plots': ['plot1.pdf', 'plot2.pdf']
}
yaml_path = Path(temp_dir) / 'metadata.yaml'
with open(yaml_path, 'w') as f:
yaml.dump(metadata, f)
# Read back and verify
with open(yaml_path, 'r') as f:
loaded = yaml.safe_load(f)
assert loaded['title'] == 'Test Gallery'
assert len(loaded['plots']) == 2
def test_metadata_module(self):
"""Test metadata module if available."""
try:
from orchestration.metadata import load_metadata_file
# Test with minimal functionality
assert callable(load_metadata_file)
except ImportError:
pytest.skip("Metadata module not available")
class TestGalleryGeneration:
"""Test gallery generation workflow."""
def test_template_processing(self, temp_dir):
"""Test Jinja2 template processing."""
from jinja2 import Template
template_content = """
<html>
<title>{{ title }}</title>
<body>
{% for plot in plots %}
<img src="{{ plot }}" alt="Plot {{ loop.index }}">
{% endfor %}
</body>
</html>
"""
template = Template(template_content)
result = template.render(
title='Test Gallery',
plots=['plot1.png', 'plot2.png']
)
assert 'Test Gallery' in result
assert 'plot1.png' in result
assert 'plot2.png' in result
def test_gallery_workflow(self, temp_dir, mock_pdf_content):
"""Test complete gallery workflow simulation."""
# Create mock directory structure
input_dir = Path(temp_dir) / 'input'
output_dir = Path(temp_dir) / 'output'
input_dir.mkdir()
output_dir.mkdir()
# Create mock PDF
pdf_path = input_dir / 'test.pdf'
pdf_path.write_bytes(mock_pdf_content)
# Create metadata
import yaml
metadata = {'title': 'Test', 'description': 'Test gallery'}
meta_path = input_dir / 'metadata.yaml'
with open(meta_path, 'w') as f:
yaml.dump(metadata, f)
# Verify setup
assert pdf_path.exists()
assert meta_path.exists()
assert output_dir.exists()
if __name__ == '__main__':
pytest.main([__file__, '-v'])
-144
View File
@@ -1,144 +0,0 @@
"""
Simple coverage test that actually works
"""
import unittest
import sys
import tempfile
import shutil
from pathlib import Path
# Add the project root to Python path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
class TestSimpleCoverage(unittest.TestCase):
"""Simple tests that will give us coverage data."""
def test_basic_imports(self):
"""Test that we can import basic modules."""
# These should work
import os
import sys
import pathlib
self.assertTrue(os.path.exists('/'))
self.assertIsNotNone(sys.version)
self.assertIsNotNone(pathlib.Path.cwd())
def test_orchestration_config(self):
"""Test config module import and basic functionality."""
try:
from orchestration.config import Config, PathConfig, GalleryConfig, UIConfig
# Test PathConfig creation
path_config = PathConfig(
work_dir="/tmp",
web_folder="/tmp/web"
)
self.assertEqual(path_config.work_dir, "/tmp")
self.assertEqual(path_config.web_folder, "/tmp/web")
# Test Config class exists
self.assertTrue(hasattr(Config, 'from_yaml'))
# Test GalleryConfig
gallery_config = GalleryConfig(
plot_root="/plots",
png_dpi=150,
backup_folder="/backup"
)
self.assertEqual(gallery_config.plot_root, "/plots")
self.assertEqual(gallery_config.png_dpi, 150)
# Test UIConfig
ui_config = UIConfig(
max_recent_plots=10,
search_debounce_ms=300
)
self.assertEqual(ui_config.max_recent_plots, 10)
self.assertEqual(ui_config.search_debounce_ms, 300)
except ImportError:
self.skipTest("Config module not available")
def test_orchestration_metadata(self):
"""Test metadata module functions."""
try:
from orchestration.metadata import merge_metadata, load_folder_metadata
# Test merge_metadata function
base = {"title": "Base Title", "author": "Base Author"}
override = {"title": "Override Title", "type": "plot"}
merged = merge_metadata(base, override)
# Override should win for title
self.assertEqual(merged["title"], "Override Title")
# Base should be preserved for author
self.assertEqual(merged["author"], "Base Author")
# New field should be added
self.assertEqual(merged["type"], "plot")
# Test empty metadata
empty_base = {}
empty_merged = merge_metadata(empty_base, override)
self.assertEqual(empty_merged["title"], "Override Title")
# Test load_folder_metadata with non-existent path
temp_dir = tempfile.mkdtemp()
try:
folder_meta = load_folder_metadata(Path(temp_dir))
self.assertIsInstance(folder_meta, dict)
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
except ImportError:
self.skipTest("Metadata module not available")
def test_file_operations(self):
"""Test basic file operations that generate coverage."""
# Create temp directory
temp_dir = tempfile.mkdtemp()
try:
# Create a test file
test_file = Path(temp_dir) / "test.txt"
test_file.write_text("Hello, World!")
# Verify file exists and has content
self.assertTrue(test_file.exists())
content = test_file.read_text()
self.assertEqual(content, "Hello, World!")
# Test file size
size = test_file.stat().st_size
self.assertGreater(size, 0)
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def test_path_manipulations(self):
"""Test path manipulations to generate more coverage."""
# Test various path operations
current_path = Path.cwd()
self.assertTrue(current_path.exists())
# Test path joining
test_path = current_path / "non_existent_file.txt"
self.assertFalse(test_path.exists())
# Test path parts
parts = current_path.parts
self.assertGreater(len(parts), 0)
# Test parent
parent = current_path.parent
self.assertIsInstance(parent, Path)
if __name__ == '__main__':
unittest.main()