1import unittest
2
3__all__ = (
4    "Clay",
5    "MoveLayerCollectionTesting",
6    "MoveSceneCollectionSyncTesting",
7    "MoveSceneCollectionTesting",
8    "ViewLayerTesting",
9    "compare_files",
10    "dump",
11    "get_layers",
12    "get_scene_collections",
13    "query_scene",
14    "setup_extra_arguments",
15)
16
17# ############################################################
18# Layer Collection Crawler
19# ############################################################
20
21
22def listbase_iter(data, struct, listbase):
23    element = data.get_pointer((struct, listbase, b'first'))
24    while element is not None:
25        yield element
26        element = element.get_pointer(b'next')
27
28
29def linkdata_iter(collection, data):
30    element = collection.get_pointer((data, b'first'))
31    while element is not None:
32        yield element
33        element = element.get_pointer(b'next')
34
35
36def get_layer_collection(layer_collection):
37    data = {}
38    flag = layer_collection.get(b'flag')
39
40    data['is_visible'] = (flag & (1 << 0)) != 0
41    data['is_selectable'] = (flag & (1 << 1)) != 0
42    data['is_disabled'] = (flag & (1 << 2)) != 0
43
44    scene_collection = layer_collection.get_pointer(b'scene_collection')
45    if scene_collection is None:
46        name = 'Fail!'
47    else:
48        name = scene_collection.get(b'name')
49    data['name'] = name
50
51    objects = []
52    for link in linkdata_iter(layer_collection, b'object_bases'):
53        ob_base = link.get_pointer(b'data')
54        ob = ob_base.get_pointer(b'object')
55        objects.append(ob.get((b'id', b'name'))[2:])
56    data['objects'] = objects
57
58    collections = {}
59    for nested_layer_collection in linkdata_iter(layer_collection, b'layer_collections'):
60        subname, subdata = get_layer_collection(nested_layer_collection)
61        collections[subname] = subdata
62    data['collections'] = collections
63
64    return name, data
65
66
67def get_layer(scene, layer):
68    data = {}
69    name = layer.get(b'name')
70
71    data['name'] = name
72    data['engine'] = scene.get((b'r', b'engine'))
73
74    active_base = layer.get_pointer(b'basact')
75    if active_base:
76        ob = active_base.get_pointer(b'object')
77        data['active_object'] = ob.get((b'id', b'name'))[2:]
78    else:
79        data['active_object'] = ""
80
81    objects = []
82    for link in linkdata_iter(layer, b'object_bases'):
83        ob = link.get_pointer(b'object')
84        objects.append(ob.get((b'id', b'name'))[2:])
85    data['objects'] = objects
86
87    collections = {}
88    for layer_collection in linkdata_iter(layer, b'layer_collections'):
89        subname, subdata = get_layer_collection(layer_collection)
90        collections[subname] = subdata
91    data['collections'] = collections
92
93    return name, data
94
95
96def get_layers(scene):
97    """Return all the render layers and their data"""
98    layers = {}
99    for layer in linkdata_iter(scene, b'view_layers'):
100        name, data = get_layer(scene, layer)
101        layers[name] = data
102    return layers
103
104
105def get_scene_collection_objects(collection, listbase):
106    objects = []
107    for link in linkdata_iter(collection, listbase):
108        ob = link.get_pointer(b'data')
109        if ob is None:
110            name = 'Fail!'
111        else:
112            name = ob.get((b'id', b'name'))[2:]
113        objects.append(name)
114    return objects
115
116
117def get_scene_collection(collection):
118    """"""
119    data = {}
120    name = collection.get(b'name')
121
122    data['name'] = name
123    data['objects'] = get_scene_collection_objects(collection, b'objects')
124
125    collections = {}
126    for nested_collection in linkdata_iter(collection, b'scene_collections'):
127        subname, subdata = get_scene_collection(nested_collection)
128        collections[subname] = subdata
129    data['collections'] = collections
130
131    return name, data
132
133
134def get_scene_collections(scene):
135    """Return all the scene collections ahd their data"""
136    master_collection = scene.get_pointer(b'collection')
137    return get_scene_collection(master_collection)
138
139
140def query_scene(filepath, name, callbacks):
141    """Return the equivalent to bpy.context.scene"""
142    from io_blend_utils.blend import blendfile
143
144    with blendfile.open_blend(filepath) as blend:
145        scenes = [block for block in blend.blocks if block.code == b'SC']
146        for scene in scenes:
147            if scene.get((b'id', b'name'))[2:] != name:
148                continue
149
150            return [callback(scene) for callback in callbacks]
151
152
153# ############################################################
154# Utils
155# ############################################################
156
157def dump(data):
158    import json
159    return json.dumps(
160        data,
161        sort_keys=True,
162        indent=4,
163        separators=(',', ': '),
164    )
165
166
167# ############################################################
168# Tests
169# ############################################################
170
171PDB = False
172DUMP_DIFF = True
173UPDATE_DIFF = False  # HACK used to update tests when something change
174
175
176def compare_files(file_a, file_b):
177    import filecmp
178
179    if not filecmp.cmp(
180            file_a,
181            file_b):
182
183        if DUMP_DIFF:
184            import subprocess
185            subprocess.call(["diff", "-u", file_b, file_a])
186
187        if UPDATE_DIFF:
188            import subprocess
189            subprocess.call(["cp", "-u", file_a, file_b])
190
191        if PDB:
192            import pdb
193            print("Files differ:", file_b, file_a)
194            pdb.set_trace()
195
196        return False
197
198    return True
199
200
201class ViewLayerTesting(unittest.TestCase):
202    _test_simple = False
203    _extra_arguments = []
204
205    @classmethod
206    def setUpClass(cls):
207        """Runs once"""
208        cls.pretest_parsing()
209
210    @classmethod
211    def get_root(cls):
212        """
213        return the folder with the test files
214        """
215        arguments = {}
216        for argument in cls._extra_arguments:
217            name, value = argument.split('=')
218            cls.assertTrue(name and name.startswith("--"), "Invalid argument \"{0}\"".format(argument))
219            cls.assertTrue(value, "Invalid argument \"{0}\"".format(argument))
220            arguments[name[2:]] = value.strip('"')
221
222        return arguments.get('testdir')
223
224    @classmethod
225    def pretest_parsing(cls):
226        """
227        Test if the arguments are properly set, and store ROOT
228        name has extra _ because we need this test to run first
229        """
230        root = cls.get_root()
231        cls.assertTrue(root, "Testdir not set")
232
233    def setUp(self):
234        """Runs once per test"""
235        import bpy
236        bpy.ops.wm.read_factory_settings()
237
238    def path_exists(self, filepath):
239        import os
240        self.assertTrue(
241            os.path.exists(filepath),
242            "Test file \"{0}\" not found".format(filepath))
243
244    def do_object_add(self, filepath_json, add_mode):
245        """
246        Testing for adding objects and see if they
247        go to the right collection
248        """
249        import bpy
250        import os
251        import tempfile
252        import filecmp
253
254        ROOT = self.get_root()
255        with tempfile.TemporaryDirectory() as dirpath:
256            filepath_layers = os.path.join(ROOT, 'layers.blend')
257
258            # open file
259            bpy.ops.wm.open_mainfile('EXEC_DEFAULT', filepath=filepath_layers)
260            self.rename_collections()
261
262            # create sub-collections
263            three_b = bpy.data.objects.get('T.3b')
264            three_c = bpy.data.objects.get('T.3c')
265
266            scene = bpy.context.scene
267            subzero = scene.master_collection.collections['1'].collections.new('sub-zero')
268            scorpion = subzero.collections.new('scorpion')
269            subzero.objects.link(three_b)
270            scorpion.objects.link(three_c)
271            layer = scene.view_layers.new('Fresh new Layer')
272            layer.collections.link(subzero)
273
274            # change active collection
275            layer.collections.active_index = 3
276            self.assertEqual(layer.collections.active.name, 'scorpion', "Run: test_syncing_object_add")
277
278            # change active layer
279            override = bpy.context.copy()
280            override["view_layer"] = layer
281            override["scene_collection"] = layer.collections.active.collection
282
283            # add new objects
284            if add_mode == 'EMPTY':
285                bpy.ops.object.add(override)  # 'Empty'
286
287            elif add_mode == 'CYLINDER':
288                bpy.ops.mesh.primitive_cylinder_add(override)  # 'Cylinder'
289
290            elif add_mode == 'TORUS':
291                bpy.ops.mesh.primitive_torus_add(override)  # 'Torus'
292
293            # save file
294            filepath_objects = os.path.join(dirpath, 'objects.blend')
295            bpy.ops.wm.save_mainfile('EXEC_DEFAULT', filepath=filepath_objects)
296
297            # get the generated json
298            datas = query_scene(filepath_objects, 'Main', (get_scene_collections, get_layers))
299            self.assertTrue(datas, "Data is not valid")
300
301            filepath_objects_json = os.path.join(dirpath, "objects.json")
302            with open(filepath_objects_json, "w") as f:
303                for data in datas:
304                    f.write(dump(data))
305
306            self.assertTrue(compare_files(
307                filepath_objects_json,
308                filepath_json,
309            ),
310                "Scene dump files differ")
311
312    def do_object_add_no_collection(self, add_mode):
313        """
314        Test for adding objects when no collection
315        exists in render layer
316        """
317        import bpy
318
319        # empty layer of collections
320
321        layer = bpy.context.view_layer
322        while layer.collections:
323            layer.collections.unlink(layer.collections[0])
324
325        # add new objects
326        if add_mode == 'EMPTY':
327            bpy.ops.object.add()  # 'Empty'
328
329        elif add_mode == 'CYLINDER':
330            bpy.ops.mesh.primitive_cylinder_add()  # 'Cylinder'
331
332        elif add_mode == 'TORUS':
333            bpy.ops.mesh.primitive_torus_add()  # 'Torus'
334
335        self.assertEqual(len(layer.collections), 1, "New collection not created")
336        collection = layer.collections[0]
337        self.assertEqual(len(collection.objects), 1, "New collection is empty")
338
339    def do_object_link(self, master_collection):
340        import bpy
341        self.assertEqual(master_collection.name, "Master Collection")
342        self.assertEqual(master_collection, bpy.context.scene.master_collection)
343        master_collection.objects.link(bpy.data.objects.new('object', None))
344
345    def do_scene_copy(self, filepath_json_reference, copy_mode, data_callbacks):
346        import bpy
347        import os
348        import tempfile
349        import filecmp
350
351        ROOT = self.get_root()
352        with tempfile.TemporaryDirectory() as dirpath:
353            filepath_layers = os.path.join(ROOT, 'layers.blend')
354
355            (self.path_exists(f) for f in (
356                filepath_layers,
357                filepath_json_reference,
358            ))
359
360            filepath_saved = os.path.join(dirpath, '{0}.blend'.format(copy_mode))
361            filepath_json = os.path.join(dirpath, "{0}.json".format(copy_mode))
362
363            bpy.ops.wm.open_mainfile('EXEC_DEFAULT', filepath=filepath_layers)
364            self.rename_collections()
365            bpy.ops.scene.new(type=copy_mode)
366            bpy.ops.wm.save_mainfile('EXEC_DEFAULT', filepath=filepath_saved)
367
368            datas = query_scene(filepath_saved, 'Main.001', data_callbacks)
369            self.assertTrue(datas, "Data is not valid")
370
371            with open(filepath_json, "w") as f:
372                for data in datas:
373                    f.write(dump(data))
374
375            self.assertTrue(compare_files(
376                filepath_json,
377                filepath_json_reference,
378            ),
379                "Scene copy \"{0}\" test failed".format(copy_mode.title()))
380
381    def do_object_delete(self, del_mode):
382        import bpy
383        import os
384        import tempfile
385        import filecmp
386
387        ROOT = self.get_root()
388        with tempfile.TemporaryDirectory() as dirpath:
389            filepath_layers = os.path.join(ROOT, 'layers.blend')
390            filepath_reference_json = os.path.join(ROOT, 'layers_object_delete.json')
391
392            # open file
393            bpy.ops.wm.open_mainfile('EXEC_DEFAULT', filepath=filepath_layers)
394            self.rename_collections()
395
396            # create sub-collections
397            three_b = bpy.data.objects.get('T.3b')
398            three_d = bpy.data.objects.get('T.3d')
399
400            scene = bpy.context.scene
401
402            # mangle the file a bit with some objects linked across collections
403            subzero = scene.master_collection.collections['1'].collections.new('sub-zero')
404            scorpion = subzero.collections.new('scorpion')
405            subzero.objects.link(three_d)
406            scorpion.objects.link(three_b)
407            scorpion.objects.link(three_d)
408
409            # object to delete
410            ob = three_d
411
412            # delete object
413            if del_mode == 'DATA':
414                bpy.data.objects.remove(ob, do_unlink=True)
415
416            elif del_mode == 'OPERATOR':
417                bpy.context.view_layer.update()  # update depsgraph
418                bpy.ops.object.select_all(action='DESELECT')
419                ob.select_set(True)
420                self.assertTrue(ob.select_get())
421                bpy.ops.object.delete()
422
423            # save file
424            filepath_generated = os.path.join(dirpath, 'generated.blend')
425            bpy.ops.wm.save_mainfile('EXEC_DEFAULT', filepath=filepath_generated)
426
427            # get the generated json
428            datas = query_scene(filepath_generated, 'Main', (get_scene_collections, get_layers))
429            self.assertTrue(datas, "Data is not valid")
430
431            filepath_generated_json = os.path.join(dirpath, "generated.json")
432            with open(filepath_generated_json, "w") as f:
433                for data in datas:
434                    f.write(dump(data))
435
436            self.assertTrue(compare_files(
437                filepath_generated_json,
438                filepath_reference_json,
439            ),
440                "Scene dump files differ")
441
442    def do_visibility_object_add(self, add_mode):
443        import bpy
444
445        scene = bpy.context.scene
446
447        # delete all objects of the file
448        for ob in bpy.data.objects:
449            bpy.data.objects.remove(ob, do_unlink=True)
450
451        # real test
452        layer = scene.view_layers.new('Visibility Test')
453        layer.collections.unlink(layer.collections[0])
454
455        scene_collection = scene.master_collection.collections.new("Collection")
456        layer.collections.link(scene_collection)
457
458        bpy.context.view_layer.update()  # update depsgraph
459
460        self.assertEqual(len(bpy.data.objects), 0)
461
462        # add new objects
463        if add_mode == 'EMPTY':
464            bpy.ops.object.add()  # 'Empty'
465
466        elif add_mode == 'CYLINDER':
467            bpy.ops.mesh.primitive_cylinder_add()  # 'Cylinder'
468
469        elif add_mode == 'TORUS':
470            bpy.ops.mesh.primitive_torus_add()  # 'Torus'
471
472        self.assertEqual(len(bpy.data.objects), 1)
473
474        new_ob = bpy.data.objects[0]
475        self.assertTrue(new_ob.visible_get(), "Object should be visible")
476
477    def cleanup_tree(self):
478        """
479        Remove any existent layer and collections,
480        leaving only the one view_layer we can't remove
481        """
482        import bpy
483        scene = bpy.context.scene
484        while len(scene.view_layers) > 1:
485            scene.view_layers.remove(scene.view_layers[1])
486
487        layer = scene.view_layers[0]
488        while layer.collections:
489            layer.collections.unlink(layer.collections[0])
490
491        master_collection = scene.master_collection
492        while master_collection.collections:
493            master_collection.collections.remove(master_collection.collections[0])
494
495    def rename_collections(self, collection=None):
496        """
497        Rename 'Collection 1' to '1'
498        """
499        def strip_name(collection):
500            import re
501            if collection.name.startswith("Default Collection"):
502                collection.name = '1'
503            else:
504                collection.name = re.findall(r'\d+', collection.name)[0]
505
506        if collection is None:
507            import bpy
508            collection = bpy.context.scene.master_collection
509
510        for nested_collection in collection.collections:
511            strip_name(nested_collection)
512            self.rename_collections(nested_collection)
513
514
515class MoveSceneCollectionTesting(ViewLayerTesting):
516    """
517    To be used by tests of view_layer_move_into_scene_collection
518    """
519
520    def get_initial_scene_tree_map(self):
521        collections_map = [
522            ['A', [
523                ['i', None],
524                ['ii', None],
525                ['iii', None],
526            ]],
527            ['B', None],
528            ['C', [
529                ['1', None],
530                ['2', None],
531                ['3', [
532                    ['dog', None],
533                    ['cat', None],
534                ]],
535            ]],
536        ]
537        return collections_map
538
539    def build_scene_tree(self, tree_map, collection=None, ret_dict=None):
540        """
541        Returns a flat dictionary with new scene collections
542        created from a nested tuple of nested tuples (name, tuple)
543        """
544        import bpy
545
546        if collection is None:
547            collection = bpy.context.scene.master_collection
548
549        if ret_dict is None:
550            ret_dict = {collection.name: collection}
551            self.assertEqual(collection.name, "Master Collection")
552
553        for name, nested_collections in tree_map:
554            new_collection = collection.collections.new(name)
555            ret_dict[name] = new_collection
556
557            if nested_collections:
558                self.build_scene_tree(nested_collections, new_collection, ret_dict)
559
560        return ret_dict
561
562    def setup_tree(self):
563        """
564        Cleanup file, and populate it with class scene tree map
565        """
566        self.cleanup_tree()
567        self.assertTrue(
568            hasattr(self, "get_initial_scene_tree_map"),
569            "Test class has no get_initial_scene_tree_map method implemented")
570
571        return self.build_scene_tree(self.get_initial_scene_tree_map())
572
573    def get_scene_tree_map(self, collection=None, ret_list=None):
574        """
575        Extract the scene collection tree from scene
576        Return as a nested list of nested lists (name, list)
577        """
578        import bpy
579
580        if collection is None:
581            scene = bpy.context.scene
582            collection = scene.master_collection
583
584        if ret_list is None:
585            ret_list = []
586
587        for nested_collection in collection.collections:
588            new_collection = [nested_collection.name, None]
589            ret_list.append(new_collection)
590
591            if nested_collection.collections:
592                new_collection[1] = list()
593                self.get_scene_tree_map(nested_collection, new_collection[1])
594
595        return ret_list
596
597    def compare_tree_maps(self):
598        """
599        Compare scene with expected (class defined) data
600        """
601        self.assertEqual(self.get_scene_tree_map(), self.get_reference_scene_tree_map())
602
603
604class MoveSceneCollectionSyncTesting(MoveSceneCollectionTesting):
605    """
606    To be used by tests of view_layer_move_into_scene_collection_sync
607    """
608
609    def get_initial_layers_tree_map(self):
610        layers_map = [
611            ['Layer 1', [
612                'Master Collection',
613                'C',
614                '3',
615            ]],
616            ['Layer 2', [
617                'C',
618                '3',
619                'dog',
620                'cat',
621            ]],
622        ]
623        return layers_map
624
625    def get_reference_layers_tree_map(self):
626        """
627        For those classes we don't expect any changes in the layer tree
628        """
629        return self.get_initial_layers_tree_map()
630
631    def setup_tree(self):
632        tree = super(MoveSceneCollectionSyncTesting, self).setup_tree()
633
634        import bpy
635        scene = bpy.context.scene
636
637        self.assertTrue(
638            hasattr(self, "get_initial_layers_tree_map"),
639            "Test class has no get_initial_layers_tree_map method implemented")
640
641        layers_map = self.get_initial_layers_tree_map()
642
643        for layer_name, collections_names in layers_map:
644            layer = scene.view_layers.new(layer_name)
645            layer.collections.unlink(layer.collections[0])
646
647            for collection_name in collections_names:
648                layer.collections.link(tree[collection_name])
649
650        return tree
651
652    def compare_tree_maps(self):
653        """
654        Compare scene with expected (class defined) data
655        """
656        super(MoveSceneCollectionSyncTesting, self).compare_tree_maps()
657
658        import bpy
659        scene = bpy.context.scene
660        layers_map = self.get_reference_layers_tree_map()
661
662        for layer_name, collections_names in layers_map:
663            layer = scene.view_layers.get(layer_name)
664            self.assertTrue(layer)
665            self.assertEqual(len(collections_names), len(layer.collections))
666
667            for i, collection_name in enumerate(collections_names):
668                self.assertEqual(collection_name, layer.collections[i].name)
669                self.verify_collection_tree(layer.collections[i])
670
671    def verify_collection_tree(self, layer_collection):
672        """
673        Check if the LayerCollection mimics the SceneLayer tree
674        """
675        scene_collection = layer_collection.collection
676        self.assertEqual(len(layer_collection.collections), len(scene_collection.collections))
677
678        for i, nested_collection in enumerate(layer_collection.collections):
679            self.assertEqual(nested_collection.collection.name, scene_collection.collections[i].name)
680            self.assertEqual(nested_collection.collection, scene_collection.collections[i])
681            self.verify_collection_tree(nested_collection)
682
683
684class MoveLayerCollectionTesting(MoveSceneCollectionSyncTesting):
685    """
686    To be used by tests of view_layer_move_into_layer_collection
687    """
688
689    def parse_move(self, path, sep='.'):
690        """
691        convert 'Layer 1.C.2' into:
692        bpy.context.scene.view_layers['Layer 1'].collections['C'].collections['2']
693        """
694        import bpy
695
696        paths = path.split(sep)
697        layer = bpy.context.scene.view_layers[paths[0]]
698        collections = layer.collections
699
700        for subpath in paths[1:]:
701            collection = collections[subpath]
702            collections = collection.collections
703
704        return collection
705
706    def move_into(self, src, dst):
707        layer_collection_src = self.parse_move(src)
708        layer_collection_dst = self.parse_move(dst)
709        return layer_collection_src.move_into(layer_collection_dst)
710
711    def move_above(self, src, dst):
712        layer_collection_src = self.parse_move(src)
713        layer_collection_dst = self.parse_move(dst)
714        return layer_collection_src.move_above(layer_collection_dst)
715
716    def move_below(self, src, dst):
717        layer_collection_src = self.parse_move(src)
718        layer_collection_dst = self.parse_move(dst)
719        return layer_collection_src.move_below(layer_collection_dst)
720
721
722class Clay:
723    def __init__(self, extra_kid_layer=False):
724        import bpy
725
726        self._scene = bpy.context.scene
727        self._layer = self._fresh_layer()
728        self._object = bpy.data.objects.new('guinea pig', bpy.data.meshes.new('mesh'))
729
730        # update depsgraph
731        self._layer.update()
732
733        scene_collection_grandma = self._scene.master_collection.collections.new("Grandma")
734        scene_collection_mom = scene_collection_grandma.collections.new("Mom")
735        scene_collection_kid = scene_collection_mom.collections.new("Kid")
736        scene_collection_kid.objects.link(self._object)
737
738        layer_collection_grandma = self._layer.collections.link(scene_collection_grandma)
739        layer_collection_mom = layer_collection_grandma.collections[0]
740        layer_collection_kid = layer_collection_mom.collections[0]
741
742        # store the variables
743        self._scene_collections = {
744            'grandma': scene_collection_grandma,
745            'mom': scene_collection_mom,
746            'kid': scene_collection_kid,
747        }
748        self._layer_collections = {
749            'grandma': layer_collection_grandma,
750            'mom': layer_collection_mom,
751            'kid': layer_collection_kid,
752        }
753
754        if extra_kid_layer:
755            layer_collection_extra = self._layer.collections.link(scene_collection_kid)
756            self._layer_collections['extra'] = layer_collection_extra
757
758        self._update()
759
760    def _fresh_layer(self):
761        import bpy
762
763        # remove all other objects
764        while bpy.data.objects:
765            bpy.data.objects.remove(bpy.data.objects[0])
766
767        # remove all the other collections
768        while self._scene.master_collection.collections:
769            self._scene.master_collection.collections.remove(
770                self._scene.master_collection.collections[0])
771
772        layer = self._scene.view_layers.new('Evaluation Test')
773        layer.collections.unlink(layer.collections[0])
774        bpy.context.window.view_layer = layer
775
776        # remove all other layers
777        for layer_iter in self._scene.view_layers:
778            if layer_iter != layer:
779                self._scene.view_layers.remove(layer_iter)
780
781        return layer
782
783    def _update(self):
784        """
785        Force depsgrpah evaluation
786        and update pointers to IDProperty collections
787        """
788        ENGINE = 'BLENDER_CLAY'
789
790        self._layer.update()  # flush depsgraph evaluation
791
792        # change scene settings
793        self._properties = {
794            'scene': self._scene.collection_properties[ENGINE],
795            'object': self._object.collection_properties[ENGINE],
796        }
797
798        for key, value in self._layer_collections.items():
799            self._properties[key] = self._layer_collections[key].engine_overrides[ENGINE]
800
801    def get(self, name, data_path):
802        self._update()
803        return getattr(self._properties[name], data_path)
804
805    def set(self, name, data_path, value):
806        self._update()
807        self._properties[name].use(data_path)
808        setattr(self._properties[name], data_path, value)
809
810
811def setup_extra_arguments(filepath):
812    """
813    Create a value which is assigned to: ``UnitTesting._extra_arguments``
814    """
815    import sys
816
817    extra_arguments = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
818    sys.argv = [filepath] + extra_arguments[1:]
819
820    return extra_arguments
821