blender-asset-tracer/tests/test_tracer.py
Sybren A. Stüvel 0bd18594f6 Support linked collections used as input in a Geometry Nodes modifier
Add support for linked collections that are used as input in a Geometry
Nodes modifier. This requires iterating over the geometry nodes modifier
settings, which consists of ID properties. If such an ID property is of
type `IDP_ID`, its pointer is followed and the pointed-to datablock +
its library are visited.

This following of pointers happens in the 'expand' phase, which was only
done for linked library blend files. Since this commit, the old
behaviour of simply looping over all non-`DATA` datablocks of the
to-be-packed blend file is not enough, and datablock expansion is done
for all local datablocks as well.
2021-07-27 16:42:25 +02:00

504 lines
16 KiB
Python

import collections
import logging
import sys
import typing
from blender_asset_tracer import trace, blendfile
from blender_asset_tracer.blendfile import dna
from tests.abstract_test import AbstractBlendFileTest
# Mimicks a BlockUsage, but without having to set the block to an expected value.
Expect = collections.namedtuple(
"Expect", "type full_field dirname_field basename_field asset_path is_sequence"
)
class AbstractTracerTest(AbstractBlendFileTest):
@classmethod
def setUpClass(cls):
super().setUpClass()
logging.getLogger("blender_asset_tracer.tracer").setLevel(logging.DEBUG)
class AssetHoldingBlocksTest(AbstractTracerTest):
def setUp(self):
self.bf = blendfile.BlendFile(self.blendfiles / "basic_file.blend")
def test_simple_file(self):
# This file should not depend on external assets.
blocks_seen = 0
seen_scene = seen_ob = False
for block in trace.asset_holding_blocks(self.bf.blocks):
assert isinstance(block, blendfile.BlendFileBlock)
blocks_seen += 1
# The four-letter-code blocks don't refer to assets, so they
# shouldn't be yielded.
self.assertEqual(2, len(block.code))
# World blocks should not yielded either.
self.assertNotEqual(b"WO", block.code)
# Do some arbitrary tests that convince us stuff is read well.
if block.code == b"SC":
seen_scene = True
self.assertEqual(b"SCScene", block.id_name)
continue
if block.code == b"OB":
seen_ob = True
self.assertEqual("OBümlaut", block.get((b"id", b"name"), as_str=True))
continue
self.assertTrue(seen_scene)
self.assertTrue(seen_ob)
# Many of the data blocks are skipped, because asset_holding_blocks() only
# yields top-level, directly-understandable blocks.
#
# The numbers here are taken from whatever the code does now; I didn't
# count the actual blocks in the actual blend file.
self.assertEqual(965, len(self.bf.blocks))
self.assertEqual(4, blocks_seen)
class DepsTest(AbstractTracerTest):
@staticmethod
def field_name(field: dna.Field) -> typing.Optional[str]:
if field is None:
return None
return field.name.name_full.decode()
def assert_deps(self, blend_fname, expects: dict):
for dep in trace.deps(self.blendfiles / blend_fname):
actual_type = dep.block.dna_type.dna_type_id.decode()
actual_full_field = self.field_name(dep.path_full_field)
actual_dirname = self.field_name(dep.path_dir_field)
actual_basename = self.field_name(dep.path_base_field)
actual = Expect(
actual_type,
actual_full_field,
actual_dirname,
actual_basename,
dep.asset_path,
dep.is_sequence,
)
exp = expects.get(dep.block_name, None)
if isinstance(exp, (set, list)):
self.assertIn(actual, exp, msg="for block %s" % dep.block_name)
exp.remove(actual)
if not exp:
# Don't leave empty sets in expects.
del expects[dep.block_name]
elif exp is None:
self.assertIsNone(
actual, msg="unexpected dependency of block %s" % dep.block_name
)
del expects[dep.block_name]
else:
self.assertEqual(exp, actual, msg="for block %s" % dep.block_name)
del expects[dep.block_name]
# All expected uses should have been seen.
self.assertEqual(expects, {}, "Expected results were not seen.")
def test_no_deps(self):
self.assert_deps("basic_file.blend", {})
def test_ob_mat_texture(self):
expects = {
b"IMbrick_dotted_04-bump": Expect(
"Image",
"name[1024]",
None,
None,
b"//textures/Bricks/brick_dotted_04-bump.jpg",
False,
),
b"IMbrick_dotted_04-color": Expect(
"Image",
"name[1024]",
None,
None,
b"//textures/Bricks/brick_dotted_04-color.jpg",
False,
),
# This data block is in there, but the image is packed, so it
# shouldn't be in the results.
# b'IMbrick_dotted_04-specular': Expect(
# 'Image', 'name[1024]', None, None,
# b'//textures/Bricks/brick_dotted_04-specular.jpg', False),
b"IMbuildings_roof_04-color": Expect(
"Image",
"name[1024]",
None,
None,
b"//textures/Textures/Buildings/buildings_roof_04-color.png",
False,
),
}
self.assert_deps("material_textures.blend", expects)
def test_seq_image_sequence(self):
expects = {
b"-unnamed-": [
Expect(
"Strip", None, "dir[768]", "name[256]", b"//imgseq/000210.png", True
),
# Video strip reference.
Expect(
"Strip",
None,
"dir[768]",
"name[256]",
b"//../../../../cloud/pillar/testfiles/video-tiny.mkv",
False,
),
# The sound will be referenced twice, from the sequence strip and an SO data block.
Expect(
"Strip",
None,
"dir[768]",
"name[256]",
b"//../../../../cloud/pillar/testfiles/video-tiny.mkv",
False,
),
],
b"SOvideo-tiny.mkv": Expect(
"bSound",
"name[1024]",
None,
None,
b"//../../../../cloud/pillar/testfiles/video-tiny.mkv",
False,
),
}
self.assert_deps("image_sequencer.blend", expects)
# Test the filename expansion.
expected = [
self.blendfiles / ("imgseq/%06d.png" % num) for num in range(210, 215)
]
for dep in trace.deps(self.blendfiles / "image_sequencer.blend"):
if dep.block_name != b"SQ000210.png":
continue
actual = list(dep.files())
self.assertEqual(actual, expected)
def test_block_cf(self):
self.assert_deps(
"alembic-user.blend",
{
b"CFclothsim.abc": Expect(
"CacheFile", "filepath[1024]", None, None, b"//clothsim.abc", False
),
},
)
def test_alembic_sequence(self):
self.assert_deps(
"alembic-sequence-user.blend",
{
b"CFclothsim_alembic": Expect(
"CacheFile",
"filepath[1024]",
None,
None,
b"//clothsim.030.abc",
True,
),
},
)
# Test the filename expansion.
expected = [
self.blendfiles / ("clothsim.%03d.abc" % num) for num in range(30, 36)
]
performed_test = False
for dep in trace.deps(self.blendfiles / "alembic-sequence-user.blend"):
if dep.block_name != b"CFclothsim_alembic":
continue
actual = list(dep.files())
self.assertEqual(actual, expected)
performed_test = True
self.assertTrue(performed_test)
def test_block_mc(self):
self.assert_deps(
"movieclip.blend",
{
b"MCvideo.mov": Expect(
"MovieClip",
"name[1024]",
None,
None,
b"//../../../../cloud/pillar/testfiles/video.mov",
False,
),
},
)
def test_block_me(self):
self.assert_deps(
"multires_external.blend",
{
b"MECube": Expect(
"Mesh", "filename[1024]", None, None, b"//Cube.btx", False
),
},
)
def test_ocean(self):
self.assert_deps(
"ocean_modifier.blend",
{
b"OBPlane.modifiers[0]": Expect(
"OceanModifierData",
"cachepath[1024]",
None,
None,
b"//cache_ocean",
True,
),
},
)
def test_particles(self):
# This file has an empty name for the cache, which should result in some hex magic
# to create a name. See ptcache_filename() in pointcache.c.
self.assert_deps(
"T55539-particles/particle.blend",
{
b"OBCube.modifiers[0]": Expect(
"PointCache",
"name[64]",
None,
None,
b"//blendcache_particle/43756265_*.bphys",
True,
),
},
)
def test_smoke(self):
# This file has an empty name for the cache, which should result in some hex magic
# to create a name. See ptcache_filename() in pointcache.c.
self.assert_deps(
"T55542-smoke/smoke_cache.blend",
{
b"OBSmoke Domain.modifiers[0]": Expect(
"PointCache",
"name[64]",
None,
None,
b"//blendcache_smoke_cache/536D6F6B6520446F6D61696E_*.bphys",
True,
),
},
)
self.assert_deps(
"T55542-smoke/smoke_cache_vdb.blend",
{
b"OBSmoke Domain.modifiers[0]": Expect(
"PointCache",
"name[64]",
None,
None,
b"//blendcache_smoke_cache_vdb/536D6F6B6520446F6D61696E_*.vdb",
True,
),
},
)
def test_mesh_cache(self):
self.assert_deps(
"meshcache-user.blend",
{
b"OBPlane.modifiers[0]": Expect(
"MeshCacheModifierData",
"filepath[1024]",
None,
None,
b"//meshcache.mdd",
False,
),
},
)
def test_block_vf(self):
self.assert_deps(
"with_font.blend",
{
b"VFHack-Bold": Expect(
"VFont",
"name[1024]",
None,
None,
b"/usr/share/fonts/truetype/hack/Hack-Bold.ttf",
False,
),
},
)
def test_block_li(self):
self.assert_deps(
"linked_cube.blend",
{
b"LILib": Expect(
"Library", "name[1024]", None, None, b"//basic_file.blend", False
),
},
)
def test_deps_recursive(self):
self.assert_deps(
"doubly_linked.blend",
{
b"LILib": {
# From doubly_linked.blend
Expect(
"Library",
"name[1024]",
None,
None,
b"//linked_cube.blend",
False,
),
# From linked_cube.blend
Expect(
"Library",
"name[1024]",
None,
None,
b"//basic_file.blend",
False,
),
},
b"LILib.002": Expect(
"Library",
"name[1024]",
None,
None,
b"//material_textures.blend",
False,
),
# From material_texture.blend
b"IMbrick_dotted_04-bump": Expect(
"Image",
"name[1024]",
None,
None,
b"//textures/Bricks/brick_dotted_04-bump.jpg",
False,
),
b"IMbrick_dotted_04-color": Expect(
"Image",
"name[1024]",
None,
None,
b"//textures/Bricks/brick_dotted_04-color.jpg",
False,
),
# This data block is in the basic_file.blend file, but not used by
# any of the objects linked in from linked_cube.blend or
# doubly_linked.blend, hence it should *not* be reported:
# b'IMbuildings_roof_04-color': Expect(
# 'Image', 'name[1024]', None, None,
# b'//textures/Textures/Buildings/buildings_roof_04-color.png', False),
},
)
def test_geometry_nodes(self):
self.assert_deps(
"geometry-nodes/file_to_pack.blend",
{
b"LInode_lib.blend": Expect(
type="Library",
full_field="name[1024]",
dirname_field=None,
basename_field=None,
asset_path=b"//node_lib.blend",
is_sequence=False,
),
b"LIobject_lib.blend": Expect(
type="Library",
full_field="name[1024]",
dirname_field=None,
basename_field=None,
asset_path=b"//object_lib.blend",
is_sequence=False,
),
},
)
def test_geometry_nodes_modifier_input(self):
"""Test linked collection as input to geom nodes modifier.
Here a Geometry Nodes modifier references a collection that is not
instanced into the scene, which caused it to be missed.
"""
self.assert_deps(
"geometry-nodes-2/shot_file.blend",
{
b"LIset_file.blend": Expect(
type="Library",
full_field="name[1024]",
dirname_field=None,
basename_field=None,
asset_path=b"//set_file.blend",
is_sequence=False,
),
b"LIlib_trash.blend": Expect(
type="Library",
full_field="name[1024]",
dirname_field=None,
basename_field=None,
asset_path=b"//lib_trash.blend",
is_sequence=False,
),
},
)
def test_usage_abspath(self):
deps = [
dep
for dep in trace.deps(self.blendfiles / "doubly_linked.blend")
if dep.asset_path == b"//material_textures.blend"
]
usage = deps[0]
expect = self.blendfiles / "material_textures.blend"
self.assertEqual(expect, usage.abspath)
def test_sim_data(self):
self.assert_deps(
"T53562/bam_pack_bug.blend",
{
b"OBEmitter.modifiers[0]": Expect(
"PointCache",
"name[64]",
None,
None,
b"//blendcache_bam_pack_bug/particles_*.bphys",
True,
),
},
)
def test_recursion_loop(self):
infinite_bfile = self.blendfiles / "recursive_dependency_1.blend"
reclim = sys.getrecursionlimit()
try:
sys.setrecursionlimit(100)
# This should finish without hitting the recursion limit.
for _ in trace.deps(infinite_bfile):
pass
finally:
sys.setrecursionlimit(reclim)