blender-asset-tracer/tests/test_tracer.py
2022-02-18 16:12:41 +01:00

544 lines
18 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_seq_image_udim_sequence(self):
expects = {
b"IMcube_UDIM.color": Expect(
'Image',
'name[1024]',
None,
None,
b'//cube_UDIM.color.<UDIM>.png',
True,
),
}
self.assert_deps("udim/v01_UDIM_BAT_debugging.blend", expects)
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_geometry_nodes_node_input(self):
"""Test linked collection as input to a geom node.
Here a Geometry Nodes node references a collection that is not instanced
into the scene, which caused it to be missed.
"""
self.assert_deps(
"geometry-nodes-3/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)