"""
Tests for the `.ifsjson` loader.
Every failure mode here is silent: a document that loads but maps parameters,
weights or the camera wrongly renders a fractal, just not the right one. So the
tests target the specific confusions the format invites, and the shipped templates
are loaded as a corpus so a real document cannot quietly stop working.
Run: python3 -m unittest discover -s blender
"""
import os
import unittest
import ifs_document
from ifs_codegen import load_library
from ifs_document import DocumentError, from_dict
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEMPLATES = os.path.join(REPO, "WpfDisplay", "Library", "Templates")
LIBRARY = load_library(os.path.join(REPO, "Transforms"),
os.path.join(REPO, "WpfDisplay", "Library", "Includes"))
def doc(**overrides):
base = {
"Iterators": [
{"Transform": {"Item1": "Affine", "Item2": "1.1"},
"Vec3Params": {"Translate": {"X": 1.0, "Y": 2.0, "Z": 3.0}},
"StartWeight": 1.0, "BaseWeight": 1.0},
],
"Camera": {"Position": {"X": 0.0, "Y": 0.0, "Z": -2.5}},
"ImageResolution": "800, 600",
}
base.update(overrides)
return from_dict(base, LIBRARY)
class ParameterMapping(unittest.TestCase):
def test_parameters_follow_the_plugins_order_not_the_documents(self):
# The kernel indexes real_params[index + N] by the PLUGIN's declaration
# order. Affine declares Translate, Scale, Rotate; a document listing them
# in another order must still map positionally.
loaded = from_dict({
"Iterators": [{
"Transform": {"Item1": "Affine", "Item2": "1.1"},
"Vec3Params": {
"Rotate": {"X": 9.0, "Y": 9.0, "Z": 9.0},
"Translate": {"X": 1.0, "Y": 1.0, "Z": 1.0},
"Scale": {"X": 5.0, "Y": 5.0, "Z": 5.0},
},
"StartWeight": 1.0}],
}, LIBRARY)
it = loaded.iterators[0]
self.assertEqual((1.0, 1.0, 1.0), it.vec3_params[0]) # Translate
self.assertEqual((5.0, 5.0, 5.0), it.vec3_params[1]) # Scale
self.assertEqual((9.0, 9.0, 9.0), it.vec3_params[2]) # Rotate
def test_a_missing_parameter_falls_back_to_the_plugin_default(self):
# Affine reads @Scale unconditionally; defaulting it to the plugin's 1 1 1
# is what stops an older document from rendering nothing.
it = doc().iterators[0]
self.assertEqual(3, len(it.vec3_params))
self.assertEqual((1.0, 1.0, 1.0), it.vec3_params[1])
def test_parameter_indices_accumulate_across_iterators(self):
loaded = from_dict({
"Iterators": [
{"Transform": {"Item1": "Affine", "Item2": "1.1"}, "StartWeight": 1.0},
{"Transform": {"Item1": "Affine", "Item2": "1.1"}, "StartWeight": 1.0},
]}, LIBRARY)
self.assertEqual([0, 3], [i.vec3_params_index for i in loaded.iterators])
def test_an_unknown_transform_is_reported_not_ignored(self):
with self.assertRaises(DocumentError) as caught:
from_dict({"Iterators": [
{"Transform": {"Item1": "NoSuchTransform", "Item2": "1"},
"StartWeight": 1.0}]}, LIBRARY)
self.assertIn("NoSuchTransform", str(caught.exception))
class WeightsAndXaos(unittest.TestCase):
def test_start_weight_drives_reset_not_the_xaos_rows(self):
# StartWeight -> reset_prob/reset_alias; BaseWeight modulates xaos. Mixing
# them up renders a differently-weighted fractal, not an error.
loaded = from_dict({"Iterators": [
{"Transform": {"Item1": "Affine", "Item2": "1.1"},
"StartWeight": 3.0, "BaseWeight": 1.0},
{"Transform": {"Item1": "Affine", "Item2": "1.1"},
"StartWeight": 1.0, "BaseWeight": 7.0},
]}, LIBRARY)
self.assertEqual([1.0, 7.0], [i.base_weight for i in loaded.iterators])
# A 3:1 start split cannot leave both reset_probs at 1.
self.assertNotEqual(loaded.iterators[0].reset_prob,
loaded.iterators[1].reset_prob)
def test_all_zero_start_weights_is_rejected(self):
# The engine returns early and renders nothing; failing loudly is better.
with self.assertRaises(DocumentError):
from_dict({"Iterators": [
{"Transform": {"Item1": "Affine", "Item2": "1.1"}, "StartWeight": 0.0}
]}, LIBRARY)
def test_xaos_is_read_as_source_by_destination(self):
loaded = from_dict({
"Iterators": [
{"Transform": {"Item1": "Affine", "Item2": "1.1"}, "StartWeight": 1.0},
{"Transform": {"Item1": "Affine", "Item2": "1.1"}, "StartWeight": 1.0},
],
"xaos": [[0.0, 2.0], [3.0, 0.0]],
}, LIBRARY)
self.assertEqual({1: 2.0}, loaded.iterators[0].weight_to)
self.assertEqual({0: 3.0}, loaded.iterators[1].weight_to)
def test_a_missing_xaos_means_fully_connected(self):
loaded = doc()
self.assertEqual({0: 1.0}, loaded.iterators[0].weight_to)
def test_a_mis_sized_xaos_is_rejected(self):
with self.assertRaises(DocumentError):
from_dict({
"Iterators": [
{"Transform": {"Item1": "Affine", "Item2": "1.1"}, "StartWeight": 1.0}],
"xaos": [[1.0, 0.0], [0.0, 1.0]],
}, LIBRARY)
class CameraAndMetadata(unittest.TestCase):
def test_identity_orientation_looks_along_positive_z(self):
camera = doc().camera
self.assertAlmostEqual(1.0, camera.forward[2], places=6)
self.assertAlmostEqual(0.0, camera.forward[0], places=6)
def test_orientation_rotates_the_direction_vectors(self):
# 180 degrees about Y: forward flips to -Z.
loaded = doc(Camera={"Position": {"X": 0.0, "Y": 0.0, "Z": 0.0},
"Orientation": {"X": 0.0, "Y": 1.0, "Z": 0.0, "W": 0.0}})
self.assertAlmostEqual(-1.0, loaded.camera.forward[2], places=6)
def test_resolution_parses_the_serialised_string(self):
self.assertEqual((800, 600), (doc().width, doc().height))
def test_legacy_palette_key_is_accepted(self):
# Documents predating the palette-manager merge store "Colors", and every
# shipped template still does.
loaded = doc(Palette={"Colors": [{"X": 1.0, "Y": 0.0, "Z": 0.0, "W": 1.0}]})
self.assertEqual([(1.0, 0.0, 0.0, 1.0)], loaded.palette_keys)
def test_palette_is_sampled_into_the_gradient_the_kernel_indexes(self):
loaded = doc(Palette={"Colors": [{"X": 1.0, "Y": 0.0, "Z": 0.0, "W": 1.0},
{"X": 0.0, "Y": 0.0, "Z": 1.0, "W": 1.0}]})
ramp = loaded.palette(150)
self.assertEqual(150, len(ramp))
self.assertAlmostEqual(1.0, ramp[0][0], places=5)
self.assertAlmostEqual(1.0, ramp[-1][2], places=5)
class ShippedTemplates(unittest.TestCase):
"""The real corpus. A new plugin or a format change must not break these."""
def test_every_template_loads(self):
if not os.path.isdir(TEMPLATES):
raise unittest.SkipTest("templates not found")
names = sorted(n for n in os.listdir(TEMPLATES) if n.endswith(".ifsjson"))
self.assertGreaterEqual(len(names), 5)
for name in names:
with self.subTest(template=name):
loaded = ifs_document.load(os.path.join(TEMPLATES, name), LIBRARY)
self.assertGreater(len(loaded.iterators), 0)
self.assertGreater(loaded.width, 0)
for it in loaded.iterators:
self.assertIn(it.tf_id, range(len(LIBRARY.plugins)))
self.assertGreaterEqual(it.reset_prob, 0.0)
if __name__ == "__main__":
unittest.main()