"""
Proves the bake renders the document it is GIVEN, not the file on disk.
`IFS_OT_bake_volume` used to call `ifs_volume.bake(path, ...)`, which reloads the
.ifsjson from disk. The node graph and the panel sliders both write into the LIVE
viewport session's document instead, so every edit made inside Blender was discarded
and a bake could only ever reproduce whatever the file already contained. Nothing
reported this: you got a valid volume of the wrong fractal.
The assertion that means something is differential. "bake_document exists" or "it
returned a path" would pass just as happily against a function that ignored its
argument and reloaded the file, which is the exact bug. So this bakes the same
document twice and requires an identical result (the chaos game is deterministically
seeded), then mutates one iterator and requires a DIFFERENT one.
Needs a GPU and openvdb, so it runs inside Blender. --factory-startup is REQUIRED:
without it the enabled add-on has already imported ifs_volume from the INSTALLED
copy, and sys.path cannot displace a module that is already in sys.modules - so the
test silently exercises whatever was last installed rather than the source tree.
blender -b --factory-startup -noaudio --python blender/test_bake_follows_document.py
"""
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEMPLATE = os.path.join(REPO, "WpfDisplay", "Library", "Templates",
"spherical_gasket_base.ifsjson")
# Small and short: this asserts WHICH document was baked, not how good the bake is.
SIZE = 32
DISPATCHES = 8
class BakeFollowsDocument(unittest.TestCase):
@classmethod
def setUpClass(cls):
try:
import gpu
import openvdb # noqa: F401
import ifs_volume # noqa: F401
except ImportError as ex:
raise unittest.SkipTest(f"needs Blender's gpu/openvdb modules ({ex})")
# build_shader() compiles, and compiling needs a live GPU context. bake() does
# this itself, so without it here the outcome depends on test ORDER.
gpu.init()
if not os.path.isfile(TEMPLATE):
raise unittest.SkipTest("template document not found")
def _load(self):
import ifs_document
import ifs_volume
_shader, library = ifs_volume.build_shader()
return ifs_document.load(TEMPLATE, library)
def _bake(self, doc, tag):
import ifs_volume
tmp = tempfile.mkdtemp()
path = os.path.join(tmp, f"{tag}.vdb")
_written, stats = ifs_volume.bake_document(
doc, path, size=SIZE, dispatches=DISPATCHES)
return stats
def test_the_same_document_bakes_the_same_way(self):
"""Establishes the baseline: without this, a difference below proves nothing,
because it could just be the chaos game's randomness."""
doc = self._load()
first = self._bake(doc, "a")
second = self._bake(doc, "b")
self.assertEqual(first["occupied_voxels"], second["occupied_voxels"],
"the bake is not deterministic, so the mutation test below "
"cannot distinguish a real change from noise")
def test_editing_the_document_changes_the_bake(self):
"""
The actual claim. A bake that reloaded the file would be identical here,
because the file was never touched.
"""
doc = self._load()
before = self._bake(doc, "before")
# Silence an iterator. Opacity is what the panel and the node graph both drive,
# and a muted iterator plots nothing, so the occupancy must move.
self.assertGreater(len(doc.iterators), 1, "template has too few iterators")
doc.iterators[0].opacity = 0.0
after = self._bake(doc, "after")
self.assertNotEqual(
before["occupied_voxels"], after["occupied_voxels"],
"muting an iterator changed nothing — the bake is not reading the "
"document it was handed, which is the bug this exists to catch")
def test_the_file_path_route_still_works(self):
"""`bake` is still the right call for "bake this file"; it must not have been
broken by the refactor."""
import ifs_volume
tmp = tempfile.mkdtemp()
path = os.path.join(tmp, "from-file.vdb")
written, stats = ifs_volume.bake(
TEMPLATE, path, size=SIZE, dispatches=DISPATCHES)
self.assertTrue(os.path.isfile(written))
self.assertGreater(stats["occupied_voxels"], 0)
if __name__ == "__main__":
unittest.main(argv=sys.argv[:1], exit=False, verbosity=2)