Files
blender/tests/python/render_layer/render_layer_common.py
Dalai Felinto 53d59af364 Corrections for layers unittest based on design change
Example, imagine an object Cube in collections 1 and 2 where both
collections are nested to A. Now we set a "color" property as follow:

```
Scene -> GREEN
--
A     -> RED
↳ 1   -> BLUE
↳ 2   -> -
```

In this case the object will be RED, because of A↳ 2.

Now if we have:

```
Scene -> GREEN
--
A     -> RED
↳ 1   -> -
↳ 2   -> PINK
1     -> -
--

The object will be PINK because of A↳ 2.

Note that the (top level) collection 1 doesn't influence the object color
because there are no overrides on it. The scene render settings (GREEN
in this case) are only used as fallback if an override is not set at
all.
2017-04-20 11:00:32 +02:00

786 lines
25 KiB
Python

import unittest
# ############################################################
# Layer Collection Crawler
# ############################################################
def listbase_iter(data, struct, listbase):
element = data.get_pointer((struct, listbase, b'first'))
while element is not None:
yield element
element = element.get_pointer(b'next')
def linkdata_iter(collection, data):
element = collection.get_pointer((data, b'first'))
while element is not None:
yield element
element = element.get_pointer(b'next')
def get_layer_collection(layer_collection):
data = {}
flag = layer_collection.get(b'flag')
data['is_visible'] = (flag & (1 << 0)) != 0
data['is_selectable'] = (flag & (1 << 1)) != 0
data['is_folded'] = True
scene_collection = layer_collection.get_pointer(b'scene_collection')
if scene_collection is None:
name = 'Fail!'
else:
name = scene_collection.get(b'name')
data['name'] = name
objects = []
for link in linkdata_iter(layer_collection, b'object_bases'):
ob_base = link.get_pointer(b'data')
ob = ob_base.get_pointer(b'object')
objects.append(ob.get((b'id', b'name'))[2:])
data['objects'] = objects
collections = {}
for nested_layer_collection in linkdata_iter(layer_collection, b'layer_collections'):
subname, subdata = get_layer_collection(nested_layer_collection)
collections[subname] = subdata
data['collections'] = collections
return name, data
def get_layer(layer):
data = {}
name = layer.get(b'name')
data['name'] = name
data['active_object'] = layer.get((b'basact', b'object', b'id', b'name'))[2:]
data['engine'] = layer.get(b'engine')
objects = []
for link in linkdata_iter(layer, b'object_bases'):
ob = link.get_pointer(b'object')
objects.append(ob.get((b'id', b'name'))[2:])
data['objects'] = objects
collections = {}
for layer_collection in linkdata_iter(layer, b'layer_collections'):
subname, subdata = get_layer_collection(layer_collection)
collections[subname] = subdata
data['collections'] = collections
return name, data
def get_layers(scene):
"""Return all the render layers and their data"""
layers = {}
for layer in linkdata_iter(scene, b'render_layers'):
name, data = get_layer(layer)
layers[name] = data
return layers
def get_scene_collection_objects(collection, listbase):
objects = []
for link in linkdata_iter(collection, listbase):
ob = link.get_pointer(b'data')
if ob is None:
name = 'Fail!'
else:
name = ob.get((b'id', b'name'))[2:]
objects.append(name)
return objects
def get_scene_collection(collection):
""""""
data = {}
name = collection.get(b'name')
data['name'] = name
data['filter'] = collection.get(b'filter')
data['objects'] = get_scene_collection_objects(collection, b'objects')
data['filter_objects'] = get_scene_collection_objects(collection, b'filter_objects')
collections = {}
for nested_collection in linkdata_iter(collection, b'scene_collections'):
subname, subdata = get_scene_collection(nested_collection)
collections[subname] = subdata
data['collections'] = collections
return name, data
def get_scene_collections(scene):
"""Return all the scene collections ahd their data"""
master_collection = scene.get_pointer(b'collection')
return get_scene_collection(master_collection)
def query_scene(filepath, name, callbacks):
"""Return the equivalent to bpy.context.scene"""
from io_blend_utils.blend import blendfile
with blendfile.open_blend(filepath) as blend:
scenes = [block for block in blend.blocks if block.code == b'SC']
for scene in scenes:
if scene.get((b'id', b'name'))[2:] != name:
continue
return [callback(scene) for callback in callbacks]
# ############################################################
# Utils
# ############################################################
def dump(data):
import json
return json.dumps(
data,
sort_keys=True,
indent=4,
separators=(',', ': '),
)
# ############################################################
# Tests
# ############################################################
PDB = False
DUMP_DIFF = True
def compare_files(file_a, file_b):
import filecmp
if not filecmp.cmp(
file_a,
file_b):
if DUMP_DIFF:
import subprocess
subprocess.call(["diff", "-u", file_a, file_b])
if PDB:
import pdb
print("Files differ:", file_a, file_b)
pdb.set_trace()
return False
return True
class RenderLayerTesting(unittest.TestCase):
_test_simple = False
_extra_arguments = []
@classmethod
def setUpClass(cls):
"""Runs once"""
cls.pretest_parsing()
@classmethod
def get_root(cls):
"""
return the folder with the test files
"""
arguments = {}
for argument in cls._extra_arguments:
name, value = argument.split('=')
cls.assertTrue(name and name.startswith("--"), "Invalid argument \"{0}\"".format(argument))
cls.assertTrue(value, "Invalid argument \"{0}\"".format(argument))
arguments[name[2:]] = value.strip('"')
return arguments.get('testdir')
@classmethod
def pretest_parsing(cls):
"""
Test if the arguments are properly set, and store ROOT
name has extra _ because we need this test to run first
"""
root = cls.get_root()
cls.assertTrue(root, "Testdir not set")
def setUp(self):
"""Runs once per test"""
import bpy
bpy.ops.wm.read_factory_settings()
def path_exists(self, filepath):
import os
self.assertTrue(
os.path.exists(filepath),
"Test file \"{0}\" not found".format(filepath))
def do_object_add(self, filepath_json, add_mode):
"""
Testing for adding objects and see if they
go to the right collection
"""
import bpy
import os
import tempfile
import filecmp
ROOT = self.get_root()
with tempfile.TemporaryDirectory() as dirpath:
filepath_layers = os.path.join(ROOT, 'layers.blend')
# open file
bpy.ops.wm.open_mainfile('EXEC_DEFAULT', filepath=filepath_layers)
self.rename_collections()
# create sub-collections
three_b = bpy.data.objects.get('T.3b')
three_c = bpy.data.objects.get('T.3c')
scene = bpy.context.scene
subzero = scene.master_collection.collections['1'].collections.new('sub-zero')
scorpion = subzero.collections.new('scorpion')
subzero.objects.link(three_b)
scorpion.objects.link(three_c)
layer = scene.render_layers.new('Fresh new Layer')
layer.collections.link(subzero)
# change active collection
layer.collections.active_index = 3
self.assertEqual(layer.collections.active.name, 'scorpion', "Run: test_syncing_object_add")
# change active layer
override = bpy.context.copy()
override["render_layer"] = layer
override["scene_collection"] = layer.collections.active.collection
# add new objects
if add_mode == 'EMPTY':
bpy.ops.object.add(override) # 'Empty'
elif add_mode == 'CYLINDER':
bpy.ops.mesh.primitive_cylinder_add(override) # 'Cylinder'
elif add_mode == 'TORUS':
bpy.ops.mesh.primitive_torus_add(override) # 'Torus'
# save file
filepath_objects = os.path.join(dirpath, 'objects.blend')
bpy.ops.wm.save_mainfile('EXEC_DEFAULT', filepath=filepath_objects)
# get the generated json
datas = query_scene(filepath_objects, 'Main', (get_scene_collections, get_layers))
self.assertTrue(datas, "Data is not valid")
filepath_objects_json = os.path.join(dirpath, "objects.json")
with open(filepath_objects_json, "w") as f:
for data in datas:
f.write(dump(data))
self.assertTrue(compare_files(
filepath_objects_json,
filepath_json,
),
"Scene dump files differ")
def do_object_add_no_collection(self, add_mode):
"""
Test for adding objects when no collection
exists in render layer
"""
import bpy
# empty layer of collections
layer = bpy.context.render_layer
while layer.collections:
layer.collections.unlink(layer.collections[0])
# add new objects
if add_mode == 'EMPTY':
bpy.ops.object.add() # 'Empty'
elif add_mode == 'CYLINDER':
bpy.ops.mesh.primitive_cylinder_add() # 'Cylinder'
elif add_mode == 'TORUS':
bpy.ops.mesh.primitive_torus_add() # 'Torus'
self.assertEqual(len(layer.collections), 1, "New collection not created")
collection = layer.collections[0]
self.assertEqual(len(collection.objects), 1, "New collection is empty")
def do_object_link(self, master_collection):
import bpy
self.assertEqual(master_collection.name, "Master Collection")
self.assertEqual(master_collection, bpy.context.scene.master_collection)
master_collection.objects.link(bpy.data.objects.new('object', None))
def do_scene_copy(self, filepath_json_reference, copy_mode, data_callbacks):
import bpy
import os
import tempfile
import filecmp
ROOT = self.get_root()
with tempfile.TemporaryDirectory() as dirpath:
filepath_layers = os.path.join(ROOT, 'layers.blend')
(self.path_exists(f) for f in (
filepath_layers,
filepath_json_reference,
))
filepath_saved = os.path.join(dirpath, '{0}.blend'.format(copy_mode))
filepath_json = os.path.join(dirpath, "{0}.json".format(copy_mode))
bpy.ops.wm.open_mainfile('EXEC_DEFAULT', filepath=filepath_layers)
self.rename_collections()
bpy.ops.scene.new(type=copy_mode)
bpy.ops.wm.save_mainfile('EXEC_DEFAULT', filepath=filepath_saved)
datas = query_scene(filepath_saved, 'Main.001', data_callbacks)
self.assertTrue(datas, "Data is not valid")
with open(filepath_json, "w") as f:
for data in datas:
f.write(dump(data))
self.assertTrue(compare_files(
filepath_json,
filepath_json_reference,
),
"Scene copy \"{0}\" test failed".format(copy_mode.title()))
def do_object_delete(self, del_mode):
import bpy
import os
import tempfile
import filecmp
ROOT = self.get_root()
with tempfile.TemporaryDirectory() as dirpath:
filepath_layers = os.path.join(ROOT, 'layers.blend')
filepath_reference_json = os.path.join(ROOT, 'layers_object_delete.json')
# open file
bpy.ops.wm.open_mainfile('EXEC_DEFAULT', filepath=filepath_layers)
self.rename_collections()
# create sub-collections
three_b = bpy.data.objects.get('T.3b')
three_d = bpy.data.objects.get('T.3d')
scene = bpy.context.scene
# mangle the file a bit with some objects linked across collections
subzero = scene.master_collection.collections['1'].collections.new('sub-zero')
scorpion = subzero.collections.new('scorpion')
subzero.objects.link(three_d)
scorpion.objects.link(three_b)
scorpion.objects.link(three_d)
# object to delete
ob = three_d
# delete object
if del_mode == 'DATA':
bpy.data.objects.remove(ob, do_unlink=True)
elif del_mode == 'OPERATOR':
bpy.context.scene.update() # update depsgraph
bpy.ops.object.select_all(action='DESELECT')
ob.select_set(action='SELECT')
self.assertTrue(ob.select_get())
bpy.ops.object.delete()
# save file
filepath_generated = os.path.join(dirpath, 'generated.blend')
bpy.ops.wm.save_mainfile('EXEC_DEFAULT', filepath=filepath_generated)
# get the generated json
datas = query_scene(filepath_generated, 'Main', (get_scene_collections, get_layers))
self.assertTrue(datas, "Data is not valid")
filepath_generated_json = os.path.join(dirpath, "generated.json")
with open(filepath_generated_json, "w") as f:
for data in datas:
f.write(dump(data))
self.assertTrue(compare_files(
filepath_generated_json,
filepath_reference_json,
),
"Scene dump files differ")
def do_visibility_object_add(self, add_mode):
import bpy
scene = bpy.context.scene
# delete all objects of the file
for ob in bpy.data.objects:
bpy.data.objects.remove(ob, do_unlink=True)
# real test
layer = scene.render_layers.new('Visibility Test')
layer.collections.unlink(layer.collections[0])
scene.render_layers.active = layer
scene_collection = scene.master_collection.collections.new("Collection")
layer.collections.link(scene_collection)
bpy.context.scene.update() # update depsgraph
self.assertEqual(len(bpy.data.objects), 0)
# add new objects
if add_mode == 'EMPTY':
bpy.ops.object.add() # 'Empty'
elif add_mode == 'CYLINDER':
bpy.ops.mesh.primitive_cylinder_add() # 'Cylinder'
elif add_mode == 'TORUS':
bpy.ops.mesh.primitive_torus_add() # 'Torus'
self.assertEqual(len(bpy.data.objects), 1)
new_ob = bpy.data.objects[0]
self.assertTrue(new_ob.visible_get(), "Object should be visible")
def cleanup_tree(self):
"""
Remove any existent layer and collections,
leaving only the one render_layer we can't remove
"""
import bpy
scene = bpy.context.scene
while len(scene.render_layers) > 1:
scene.render_layers.remove(scene.render_layers[1])
layer = scene.render_layers[0]
while layer.collections:
layer.collections.unlink(layer.collections[0])
master_collection = scene.master_collection
while master_collection.collections:
master_collection.collections.remove(master_collection.collections[0])
def rename_collections(self, collection=None):
"""
Rename 'Collection 1' to '1'
"""
def strip_name(collection):
import re
if collection.name.startswith("Default Collection"):
collection.name = '1'
else:
collection.name = re.findall(r'\d+', collection.name)[0]
if collection is None:
import bpy
collection = bpy.context.scene.master_collection
for nested_collection in collection.collections:
strip_name(nested_collection)
self.rename_collections(nested_collection)
class MoveSceneCollectionTesting(RenderLayerTesting):
"""
To be used by tests of render_layer_move_into_scene_collection
"""
def get_initial_scene_tree_map(self):
collections_map = [
['A', [
['i', None],
['ii', None],
['iii', None],
]],
['B', None],
['C', [
['1', None],
['2', None],
['3', [
['dog', None],
['cat', None],
]],
]],
]
return collections_map
def build_scene_tree(self, tree_map, collection=None, ret_dict=None):
"""
Returns a flat dictionary with new scene collections
created from a nested tuple of nested tuples (name, tuple)
"""
import bpy
if collection is None:
collection = bpy.context.scene.master_collection
if ret_dict is None:
ret_dict = {collection.name: collection}
self.assertEqual(collection.name, "Master Collection")
for name, nested_collections in tree_map:
new_collection = collection.collections.new(name)
ret_dict[name] = new_collection
if nested_collections:
self.build_scene_tree(nested_collections, new_collection, ret_dict)
return ret_dict
def setup_tree(self):
"""
Cleanup file, and populate it with class scene tree map
"""
self.cleanup_tree()
self.assertTrue(
hasattr(self, "get_initial_scene_tree_map"),
"Test class has no get_initial_scene_tree_map method implemented")
return self.build_scene_tree(self.get_initial_scene_tree_map())
def get_scene_tree_map(self, collection=None, ret_list=None):
"""
Extract the scene collection tree from scene
Return as a nested list of nested lists (name, list)
"""
import bpy
if collection is None:
scene = bpy.context.scene
collection = scene.master_collection
if ret_list is None:
ret_list = []
for nested_collection in collection.collections:
new_collection = [nested_collection.name, None]
ret_list.append(new_collection)
if nested_collection.collections:
new_collection[1] = list()
self.get_scene_tree_map(nested_collection, new_collection[1])
return ret_list
def compare_tree_maps(self):
"""
Compare scene with expected (class defined) data
"""
self.assertEqual(self.get_scene_tree_map(), self.get_reference_scene_tree_map())
class MoveSceneCollectionSyncTesting(MoveSceneCollectionTesting):
"""
To be used by tests of render_layer_move_into_scene_collection_sync
"""
def get_initial_layers_tree_map(self):
layers_map = [
['Layer 1', [
'Master Collection',
'C',
'3',
]],
['Layer 2', [
'C',
'3',
'dog',
'cat',
]],
]
return layers_map
def get_reference_layers_tree_map(self):
"""
For those classes we don't expect any changes in the layer tree
"""
return self.get_initial_layers_tree_map()
def setup_tree(self):
tree = super(MoveSceneCollectionSyncTesting, self).setup_tree()
import bpy
scene = bpy.context.scene
self.assertTrue(
hasattr(self, "get_initial_layers_tree_map"),
"Test class has no get_initial_layers_tree_map method implemented")
layers_map = self.get_initial_layers_tree_map()
for layer_name, collections_names in layers_map:
layer = scene.render_layers.new(layer_name)
layer.collections.unlink(layer.collections[0])
for collection_name in collections_names:
layer.collections.link(tree[collection_name])
return tree
def compare_tree_maps(self):
"""
Compare scene with expected (class defined) data
"""
super(MoveSceneCollectionSyncTesting, self).compare_tree_maps()
import bpy
scene = bpy.context.scene
layers_map = self.get_reference_layers_tree_map()
for layer_name, collections_names in layers_map:
layer = scene.render_layers.get(layer_name)
self.assertTrue(layer)
self.assertEqual(len(collections_names), len(layer.collections))
for i, collection_name in enumerate(collections_names):
self.assertEqual(collection_name, layer.collections[i].name)
self.verify_collection_tree(layer.collections[i])
def verify_collection_tree(self, layer_collection):
"""
Check if the LayerCollection mimics the SceneLayer tree
"""
scene_collection = layer_collection.collection
self.assertEqual(len(layer_collection.collections), len(scene_collection.collections))
for i, nested_collection in enumerate(layer_collection.collections):
self.assertEqual(nested_collection.collection.name, scene_collection.collections[i].name)
self.assertEqual(nested_collection.collection, scene_collection.collections[i])
self.verify_collection_tree(nested_collection)
class MoveLayerCollectionTesting(MoveSceneCollectionSyncTesting):
"""
To be used by tests of render_layer_move_into_layer_collection
"""
def parse_move(self, path, sep='.'):
"""
convert 'Layer 1.C.2' into:
bpy.context.scene.render_layers['Layer 1'].collections['C'].collections['2']
"""
import bpy
paths = path.split(sep)
layer = bpy.context.scene.render_layers[paths[0]]
collections = layer.collections
for subpath in paths[1:]:
collection = collections[subpath]
collections = collection.collections
return collection
def move_into(self, src, dst):
layer_collection_src = self.parse_move(src)
layer_collection_dst = self.parse_move(dst)
return layer_collection_src.move_into(layer_collection_dst)
def move_above(self, src, dst):
layer_collection_src = self.parse_move(src)
layer_collection_dst = self.parse_move(dst)
return layer_collection_src.move_above(layer_collection_dst)
def move_below(self, src, dst):
layer_collection_src = self.parse_move(src)
layer_collection_dst = self.parse_move(dst)
return layer_collection_src.move_below(layer_collection_dst)
class Clay:
def __init__(self, extra_kid_layer=False):
import bpy
self._scene = bpy.context.scene
self._layer = self._fresh_layer()
self._object = bpy.data.objects.new('guinea pig', bpy.data.meshes.new('mesh'))
# update depsgraph
self._scene.update()
scene_collection_grandma = self._scene.master_collection.collections.new("Grandma")
scene_collection_mom = scene_collection_grandma.collections.new("Mom")
scene_collection_kid = scene_collection_mom.collections.new("Kid")
scene_collection_kid.objects.link(self._object)
layer_collection_grandma = self._layer.collections.link(scene_collection_grandma)
layer_collection_mom = layer_collection_grandma.collections[0]
layer_collection_kid = layer_collection_mom.collections[0]
# store the variables
self._scene_collections = {
'grandma': scene_collection_grandma,
'mom': scene_collection_mom,
'kid': scene_collection_kid,
}
self._layer_collections = {
'grandma': layer_collection_grandma,
'mom': layer_collection_mom,
'kid': layer_collection_kid,
}
if extra_kid_layer:
layer_collection_extra = self._layer.collections.link(scene_collection_kid)
self._layer_collections['extra'] = layer_collection_extra
self._update()
def _fresh_layer(self):
import bpy
# remove all other objects
while bpy.data.objects:
bpy.data.objects.remove(bpy.data.objects[0])
# remove all the other collections
while self._scene.master_collection.collections:
self._scene.master_collection.collections.remove(
self._scene.master_collection.collections[0])
layer = self._scene.render_layers.new('Evaluation Test')
layer.collections.unlink(layer.collections[0])
self._scene.render_layers.active = layer
# remove all other layers
for layer_iter in self._scene.render_layers:
if layer_iter != layer:
self._scene.render_layers.remove(layer_iter)
return layer
def _update(self):
"""
Force depsgrpah evaluation
and update pointers to IDProperty collections
"""
ENGINE = 'BLENDER_CLAY'
self._scene.update() # update depsgraph
self._layer.update() # flush depsgraph evaluation
# change scene settings
self._properties = {
'scene': self._scene.collection_properties[ENGINE],
'object': self._object.collection_properties[ENGINE],
}
for key, value in self._layer_collections.items():
self._properties[key] = self._layer_collections[key].engine_overrides[ENGINE]
def get(self, name, data_path):
self._update()
return getattr(self._properties[name], data_path)
def set(self, name, data_path, value):
self._update()
self._properties[name].use(data_path)
setattr(self._properties[name], data_path, value)