Skip to content

Commit

Permalink
Merge pull request #17 from HeyItsBATMAN/add-simple-testing-framework
Browse files Browse the repository at this point in the history
Add simple testing framework, for testing import and export of (for now) IIIF manifests and (later) Blender scenes
  • Loading branch information
vincentmarchetti authored Nov 23, 2024
2 parents d5d9b8d + e1bc16c commit d492067
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 0 deletions.
92 changes: 92 additions & 0 deletions .github/workflows/test_manifests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Test IIIF Manifests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test-manifests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libxrender1 \
libxxf86vm1 \
libxfixes3 \
libxi6 \
libxkbcommon0 \
libxkbcommon-x11-0 \
libgl1 \
libglu1-mesa \
libsm6 \
libxext6 \
libx11-6 \
libxcb1 \
libtinfo5
- name: Cache Blender
id: cache-blender
uses: actions/cache@v3
with:
path: /tmp/blender.tar.xz
key: blender-4.2.0

- name: Install Blender 4.2
run: |
BLENDER_VERSION="4.2.4"
BLENDER_FILE="blender-${BLENDER_VERSION}-linux-x64.tar.xz"
if [ ! -f /tmp/blender.tar.xz ]; then
# Download Blender
wget "https://download.blender.org/release/Blender4.2/${BLENDER_FILE}" -O /tmp/blender.tar.xz
fi
# Extract Blender
tar -xf /tmp/blender.tar.xz
# Move Blender to /usr/local and create symlink
sudo mv "blender-${BLENDER_VERSION}-linux-x64" /usr/local/blender
sudo ln -s /usr/local/blender/blender /usr/local/bin/blender
# Verify installation
blender --background --version
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
# Add any additional dependencies your script needs here
# pip install -r requirements.txt
- name: Build & install Blender plugin
run: |
blender --command extension build --output-filepath iiif_blender.zip
blender --command extension install-file --enable --repo user_default iiif_blender.zip
- name: Run tests
id: run-tests
run: |
echo "Running manifest tests..."
bash tests/run_tests.sh
- name: Report test results
if: always()
run: |
echo "Test execution completed"
if [ "${{ steps.run-tests.outcome }}" == "failure" ]; then
echo "❌ Tests failed"
exit 1
else
echo "✅ Tests passed"
fi
154 changes: 154 additions & 0 deletions run_blender_with_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import json
import sys
import os
from typing import Any, Dict, List, Tuple, Callable
import subprocess
import bpy
import difflib

# Ensure the script receives the correct number of arguments
if len(sys.argv) < 4:
print(
"Usage: blender --background --python run_blender_with_plugin.py -- <input_manifest>"
)
sys.exit(1)

# Get the input and output manifest file paths from the command line arguments
input_manifest = sys.argv[sys.argv.index("--") + 1]
output_manifest = input_manifest.replace(".json", "_export.json")

context = bpy.context
if context is None:
print("Failed to get the Blender context")
sys.exit(1)


def get_extension_id():
try:
manifest_path = os.path.join(os.path.dirname(__file__), "blender_manifest.toml")
with open(manifest_path, "r") as f:
for line in f:
line = line.strip()
if line.startswith("id = "):
# Extract the value between quotes
return line.split("=")[1].strip().strip('"').strip("'")
except Exception as e:
print(f"Error reading blender_manifest.toml: {e}")
sys.exit(1)
print("Could not find id in blender_manifest.toml")
sys.exit(1)


# Load the plugin
needle = get_extension_id()
ext_name = None
for key in context.preferences.addons.keys():
if needle in key:
ext_name = key
break

if not ext_name:
print("Failed to find the plugin")
sys.exit(1)

bpy.ops.preferences.addon_enable(module=ext_name)

if ext_name not in context.preferences.addons:
print("Failed to load the plugin")
sys.exit(1)


def safe_delete(file_path):
try:
# Check if file exists before attempting deletion
if os.path.exists(file_path):
os.remove(file_path)
print(f"File {file_path} has been deleted successfully")
else:
print(f"File {file_path} does not exist")
except Exception as e:
print(f"Error occurred while deleting file: {e}")


RED: Callable[[str], str] = lambda text: f"\u001b[31m{text}\033\u001b[0m"
GREEN: Callable[[str], str] = lambda text: f"\u001b[32m{text}\033\u001b[0m"


def get_edits_string(old: str, new: str) -> Tuple[str, bool]:
result = ""

lines = difflib.ndiff(old.splitlines(keepends=True), new.splitlines(keepends=True))

has_changes = False

for line in lines:
line = line.rstrip()
if line.startswith("+"):
has_changes = True
result += GREEN(line) + "\n"
elif line.startswith("-"):
has_changes = True
result += RED(line) + "\n"
elif line.startswith("?"):
continue
else:
result += line + "\n"

return (result, has_changes)


def get_json_diff(file1_path: str, file2_path: str) -> Tuple[str, bool]:
with open(file1_path) as f1:
json1 = json.load(f1)
with open(file2_path) as f2:
json2 = json.load(f2)
return get_edits_string(
json.dumps(json1, indent=2, sort_keys=True),
json.dumps(json2, indent=2, sort_keys=True),
)


def get_indent(level):
return " " * level


def print_object_hierarchy(obj, level):
indent = get_indent(level)
print(f"{indent}- {obj.name} ({obj.type})")

for child in obj.children:
print_object_hierarchy(child, level + 1)


def print_collection_hierarchy(collection, level=0):
indent = get_indent(level)
print(f"{indent}{collection.name} (Collection):")

for obj in collection.objects:
if not obj.parent:
print_object_hierarchy(obj, level + 1)

for child_col in collection.children:
print_collection_hierarchy(child_col, level + 1)


bpy.ops.import_scene.iiif_manifest(filepath=input_manifest)

print("\n\nPrinting scene hierarchy:")
print_collection_hierarchy(bpy.context.scene.collection)
print("\n\n")

bpy.ops.export_scene.iiif_manifest(filepath=output_manifest)

differences, has_changes = get_json_diff(input_manifest, output_manifest)

# Delete output manifest
safe_delete(output_manifest)

if has_changes:
print("Imported manifest differs from exported manifest:")
print(differences)
sys.exit(1)
else:
print("Imported manifest equals exported manifest")
sys.exit(0)
Empty file added tests/blender_scenes/.gitkeep
Empty file.
36 changes: 36 additions & 0 deletions tests/iiif_manifests/0101_model_origin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"@context": "http://iiif.io/api/presentation/4/context.json",
"id": "https://example.org/iiif/3d/model_origin.json",
"type": "Manifest",
"label": { "en": ["Single Model"] },
"summary": {
"en": [
"Viewer should render the model at the scene origin, and then viewer should add default lighting and camera"
]
},
"items": [
{
"id": "https://example.org/iiif/scene1/page/p1/1",
"type": "Scene",
"label": { "en": ["A Scene"] },
"items": [
{
"id": "https://example.org/iiif/scene1/page/p1/1",
"type": "AnnotationPage",
"items": [
{
"id": "https://example.org/iiif/3d/anno1",
"type": "Annotation",
"motivation": ["painting"],
"body": {
"id": "https://raw.githubusercontent.com/IIIF/3d/main/assets/astronaut/astronaut.glb",
"type": "Model"
},
"target": "https://example.org/iiif/scene1/page/p1/1"
}
]
}
]
}
]
}
43 changes: 43 additions & 0 deletions tests/iiif_manifests/0201_perspective_camera.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"@context": "http://iiif.io/api/presentation/4/context.json",
"id": "https://example.org/iiif/3d/model_origin.json",
"type": "Manifest",
"label": { "en": ["Model with Explicit Perspective Camera"] },
"summary": { "en": ["Viewer should render the model at the scene origin, and the camera at the scene origin facing -Z, then add default lighting"] },
"items": [
{
"id": "https://example.org/iiif/scene1/page/p1/1",
"type": "Scene",
"label": { "en": ["Scene with Model and Camera"] },
"items": [
{
"id": "https://example.org/iiif/scene1/page/p1/1",
"type": "AnnotationPage",
"items": [
{
"id": "https://example.org/iiif/3d/anno1",
"type": "Annotation",
"motivation": ["painting"],
"body": {
"id": "https://raw.githubusercontent.com/IIIF/3d/main/assets/astronaut/astronaut.glb",
"type": "Model"
},
"target": "https://example.org/iiif/scene1/page/p1/1"
},
{
"id": "https://example.org/iiif/3d/anno1",
"type": "Annotation",
"motivation": ["painting"],
"body": {
"id": "https://example.org/iiif/3d/cameras/1",
"type": "PerspectiveCamera",
"label": {"en": ["Perspective Camera 1"]}
},
"target": "https://example.org/iiif/scene1/page/p1/1"
}
]
}
]
}
]
}
36 changes: 36 additions & 0 deletions tests/run_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash

FAILED_TESTS=0

run_test() {
local command="$1"
local expected_code="${2:-0}"

eval "$command" > /dev/null 2>&1
local actual_code=$?

if [ "$actual_code" -eq "$expected_code" ]; then
echo "✅ Test passed: '$command' (Exit code: $actual_code)"
return 0
else
echo "❌ Test failed: '$command'"
echo "ℹ️ Expected exit code: $expected_code"
echo "ℹ️ Actual exit code: $actual_code"
return 1
fi
}

for manifest in tests/iiif_manifests/*.json; do
echo "ℹ️ Testing manifest: $manifest"
if ! run_test "blender --background --python run_blender_with_plugin.py -- '$manifest'"; then
((FAILED_TESTS++))
fi
done

if [ $FAILED_TESTS -gt 0 ]; then
echo "$FAILED_TESTS test(s) failed"
exit 1
else
echo "✅ All tests passed"
exit 0
fi

0 comments on commit d492067

Please sign in to comment.