Add Chromium-only Blender WebEngine parity work
This commit is contained in:
222
blender-5.2.0/tests/python/bl_global_undo.py
Normal file
222
blender-5.2.0/tests/python/bl_global_undo.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# ./blender.bin --background --python tests/python/bl_global_undo.py ..
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.realpath(__file__)))
|
||||
from bl_blendfile_utils import TestBlendLibLinkHelper
|
||||
|
||||
|
||||
NO_UNDO_ID_TYPES = {bpy.types.Brush, bpy.types.WindowManager, bpy.types.WorkSpace, bpy.types.Screen}
|
||||
|
||||
|
||||
class IDStaticRepresentation:
|
||||
"""
|
||||
A fully Blender-independant representation of a data-block.
|
||||
"""
|
||||
|
||||
def __init__(self, id_data, full=False):
|
||||
self.id_type = id_data.id_type
|
||||
self.name = id_data.name
|
||||
self.library = id_data.library.filepath if id_data.library else ""
|
||||
|
||||
self.session_uid = id_data.session_uid
|
||||
self.pointer = id_data.as_pointer()
|
||||
|
||||
self.content = self.extract_content(id_data) if full else {}
|
||||
|
||||
@staticmethod
|
||||
def extract_content(id_data):
|
||||
"""
|
||||
Generate a key-value (dict) representation of ID content, using RNA self-introspection mechanism.
|
||||
"""
|
||||
# TODO.
|
||||
return {}
|
||||
|
||||
def as_invariant_key(self):
|
||||
"""Key value that should remain stable and be unique to an ID, even across e.g. blendfile reload."""
|
||||
return (self.id_type, self.name, self.library)
|
||||
|
||||
def as_session_key(self):
|
||||
"""
|
||||
Key value that should remain stable and be unique to an ID, during a single session.
|
||||
Not stable across e.g. blendfile reload.
|
||||
"""
|
||||
return self.session_uid
|
||||
|
||||
def __eq__(self, other):
|
||||
if self.id_type != other.id_type:
|
||||
return False
|
||||
if self.name != other.name:
|
||||
return False
|
||||
if self.library != other.library:
|
||||
return False
|
||||
if self.content != other.content:
|
||||
# Only consider content as different if it is defined in both ID representations.
|
||||
if self.content and other.content:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.as_invariant_key())
|
||||
|
||||
|
||||
class TestGlobalUndo(TestBlendLibLinkHelper):
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
|
||||
def link(self, lib_filepath, collections_names=[]):
|
||||
operation_name = "link"
|
||||
with bpy.data.libraries.load(lib_filepath, link=True) as (lib_in, lib_out):
|
||||
for cn in collections_names:
|
||||
lib_out.collections.append(lib_in.collections[cn])
|
||||
|
||||
@staticmethod
|
||||
def get_all_ids():
|
||||
return {IDStaticRepresentation(id_data) for id_data in bpy.data.all_ids}
|
||||
|
||||
def test_global_undo_empty(self):
|
||||
self.reset_blender()
|
||||
|
||||
all_ids = self.get_all_ids()
|
||||
|
||||
# NOTE: The two undo pushes are necessary to be able to undo, since the first undo push creates the
|
||||
# initial state for memfile undo (it is not initialized by default in background mode).
|
||||
bpy.ops.ed.undo_push()
|
||||
bpy.ops.ed.undo_push()
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
# Here all IDs remain present at all time, so their address themselves should remain unchanged.
|
||||
|
||||
bpy.ops.ed.undo()
|
||||
self.assertEqual({id_data.pointer: id_data for id_data in all_ids},
|
||||
{id_data.pointer: id_data for id_data in self.get_all_ids()})
|
||||
bpy.ops.ed.undo()
|
||||
self.assertEqual({id_data.pointer: id_data for id_data in all_ids},
|
||||
{id_data.pointer: id_data for id_data in self.get_all_ids()})
|
||||
|
||||
bpy.ops.ed.redo()
|
||||
self.assertEqual({id_data.pointer: id_data for id_data in all_ids},
|
||||
{id_data.pointer: id_data for id_data in self.get_all_ids()})
|
||||
bpy.ops.ed.redo()
|
||||
self.assertEqual({id_data.pointer: id_data for id_data in all_ids},
|
||||
{id_data.pointer: id_data for id_data in self.get_all_ids()})
|
||||
|
||||
bpy.ops.ed.undo_history(item=0)
|
||||
self.assertEqual({id_data.pointer: id_data for id_data in all_ids},
|
||||
{id_data.pointer: id_data for id_data in self.get_all_ids()})
|
||||
|
||||
bpy.ops.ed.undo_history(item=2)
|
||||
self.assertEqual({id_data.pointer: id_data for id_data in all_ids},
|
||||
{id_data.pointer: id_data for id_data in self.get_all_ids()})
|
||||
|
||||
def test_global_undo_local_data(self):
|
||||
self.reset_blender()
|
||||
|
||||
all_ids_init = self.get_all_ids()
|
||||
|
||||
# NOTE: The two undo pushes are necessary to be able to undo, since the first undo push creates the
|
||||
# initial state for memfile undo (it is not initialized by default in background mode).
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
self.gen_library_data_()
|
||||
object_location_init = tuple(bpy.data.objects[0].location)
|
||||
bpy.ops.ed.undo_push()
|
||||
bpy.data.objects[0].location.x += 1.0
|
||||
object_location_final = tuple(bpy.data.objects[0].location)
|
||||
bpy.ops.ed.undo_push()
|
||||
|
||||
all_ids_final_before_undoredo = self.get_all_ids()
|
||||
|
||||
bpy.ops.ed.undo()
|
||||
self.assertEqual({id_data.pointer: id_data for id_data in all_ids_final_before_undoredo},
|
||||
{id_data.pointer: id_data for id_data in self.get_all_ids()})
|
||||
self.assertEqual(tuple(bpy.data.objects[0].location), object_location_init)
|
||||
bpy.ops.ed.redo()
|
||||
self.assertEqual({id_data.pointer: id_data for id_data in all_ids_final_before_undoredo},
|
||||
{id_data.pointer: id_data for id_data in self.get_all_ids()})
|
||||
self.assertEqual(tuple(bpy.data.objects[0].location), object_location_final)
|
||||
|
||||
bpy.ops.ed.undo()
|
||||
bpy.ops.ed.undo()
|
||||
self.assertEqual({id_data.pointer: id_data for id_data in all_ids_init},
|
||||
{id_data.pointer: id_data for id_data in self.get_all_ids()})
|
||||
|
||||
# Undoing removed all IDs generated by the call to `gen_library_data_()`, so when redoing,
|
||||
# their invariant and session identifiers whould rmain unchanged,
|
||||
# but their pointer address should be different (as redo will re-allocate them).
|
||||
bpy.ops.ed.redo()
|
||||
all_ids_final_after_undoredo = self.get_all_ids()
|
||||
self.assertNotEqual({id_data.pointer: id_data for id_data in all_ids_final_before_undoredo},
|
||||
{id_data.pointer: id_data for id_data in all_ids_final_after_undoredo})
|
||||
self.assertEqual({id_data.as_session_key(): id_data for id_data in all_ids_final_before_undoredo},
|
||||
{id_data.as_session_key(): id_data for id_data in all_ids_final_after_undoredo})
|
||||
self.assertEqual({id_data.as_invariant_key(): id_data for id_data in all_ids_final_before_undoredo},
|
||||
{id_data.as_invariant_key(): id_data for id_data in all_ids_final_after_undoredo})
|
||||
self.assertEqual(tuple(bpy.data.objects[0].location), object_location_init)
|
||||
|
||||
# Second redo should re-use all exisitng IDs, even though some of their data is modified.
|
||||
bpy.ops.ed.redo()
|
||||
self.assertEqual({id_data.pointer: id_data for id_data in all_ids_final_after_undoredo},
|
||||
{id_data.pointer: id_data for id_data in self.get_all_ids()})
|
||||
self.assertEqual(tuple(bpy.data.objects[0].location), object_location_final)
|
||||
|
||||
# TODO: add undo testing of linking, making local, linking as packed data, making liboverrides, etc.
|
||||
|
||||
|
||||
TESTS = (
|
||||
TestGlobalUndo,
|
||||
)
|
||||
|
||||
|
||||
def argparse_create():
|
||||
import argparse
|
||||
|
||||
# When --help or no args are given, print this help
|
||||
description = (
|
||||
"Test basic global (aka memfile) undo and redo component.\n"
|
||||
"\n"
|
||||
"IMPORTANT: This test should be run in Blender with `--log \"*undo*\" --log-level debug`")
|
||||
parser = argparse.ArgumentParser(description=description)
|
||||
parser.add_argument(
|
||||
"--src-test-dir",
|
||||
dest="src_test_dir",
|
||||
default=".",
|
||||
help="Where to find test/data root directory",
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
dest="output_dir",
|
||||
help="Where to output temp saved blendfiles",
|
||||
required=True,
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
args = argparse_create().parse_args()
|
||||
|
||||
for Test in TESTS:
|
||||
Test(args).run_all_tests()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.argv = [__file__] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
|
||||
main()
|
||||
Reference in New Issue
Block a user