Tests: Add basic save & reload to the 'versioning' tests.

This should allow us to catch significantly more 'sneaky' issues with
writefile and versioning codes in the future.

Pull Request: https://projects.blender.org/blender/blender/pulls/140735
This commit is contained in:
Bastien Montagne
2025-06-21 14:06:43 +02:00
committed by Bastien Montagne
parent 827fc096ae
commit da4eda148b
2 changed files with 98 additions and 17 deletions

View File

@@ -23,14 +23,15 @@ sys.path.append(os.path.dirname(os.path.realpath(__file__)))
from bl_blendfile_utils import TestHelper
class TestBlendFileOpenAllTestFiles(TestHelper):
class TestBlendFileOpenLinkSaveAllTestFiles(TestHelper):
def __init__(self, args):
self.args = args
# Some files are known broken currently.
# Some files are known broken currently for opening or linking.
# They cannot be opened, or will generate some error (e.g. memleaks).
# Each file in this list should either be the source of a bug report,
# or removed from tests repo.
self.excluded_paths = {
self.excluded_open_link_paths = {
# modifier_stack/explode_modifier.blend
# BLI_assert failed: source/blender/blenlib/BLI_ordered_edge.hh:41, operator==(), at 'e1.v_low < e1.v_high'
"explode_modifier.blend",
@@ -55,6 +56,49 @@ class TestBlendFileOpenAllTestFiles(TestHelper):
"ram_glsl.blend",
}
# Directories to exclude relative to `./tests/files/`.
self.excluded_open_link_dirs = ()
# Some files are known broken currently on re-saving & re-opening.
# Each file in this list should either be the source of a bug report,
# or removed from tests repo.
self.excluded_save_reload_paths = {
# gameengine_bullet_softbody/softbody_constraints.blend
# Error: on save,
# 'Unable to pack file, source path '.../gameengine_bullet_softbody/marble_256.jpg' not found'
"softbody_constraints.blend",
# files/libraries_and_linking/library_test_scene.blend
# Error: on save and/or reload, creates memleaks.
"library_test_scene.blend",
# files/libraries_and_linking/libraries/main_scene.blend
# Error: on save and/or reload, creates memleaks.
"main_scene.blend",
# modeling/geometry_nodes/import/import_obj.blend
# Error: on reload,
# "OBJParser: Cannot read from OBJ file:
# '/home/guest/blender/main/build_main_release/tests/blendfile_io/data_files/icosphere.obj'"
"import_obj.blend",
# grease_pencil/grease_pencil_paper_pig.blend
# Error: on save and/or reload, creates memleaks.
"grease_pencil_paper_pig.blend",
# modeling/geometry_nodes/import/import_ply.blend
# Error: on reload, 'read_ply_to_mesh: PLY Importer: icosphere: Invalid PLY header.'
"import_ply.blend",
# modeling/geometry_nodes/import/import_stl.blend
# Error: on reload,
# 'read_stl_file: Failed to open STL file:'...tests/blendfile_io/data_files/icosphere.stl'.'
"import_stl.blend",
}
# Directories to exclude relative to `./tests/files/`.
self.excluded_save_reload_dirs = ()
# Some files are expected to be invalid.
# This mapping stores filenames as keys, and expected error message as value.
self.invalid_paths = {
@@ -230,11 +274,10 @@ class TestBlendFileOpenAllTestFiles(TestHelper):
),
}
# Directories to exclude relative to `./tests/files/`.
self.excluded_dirs = ()
assert all(p.endswith("/") for p in self.excluded_dirs)
self.excluded_dirs = tuple(p.replace("/", os.sep) for p in self.excluded_dirs)
assert all(p.endswith("/") for p in self.excluded_open_link_dirs)
self.excluded_open_link_dirs = tuple(p.replace("/", os.sep) for p in self.excluded_open_link_dirs)
assert all(p.endswith("/") for p in self.excluded_save_reload_dirs)
self.excluded_save_reload_dirs = tuple(p.replace("/", os.sep) for p in self.excluded_save_reload_dirs)
# Generate the slice of blendfile paths that this instance of the test should process.
blendfile_paths = [p for p in self.iter_blendfiles_from_directory(self.args.src_test_dir)]
@@ -267,16 +310,22 @@ class TestBlendFileOpenAllTestFiles(TestHelper):
slice_indices = [(gen_indices(i), gen_indices(i + 1)) for i in range(slice_range)]
return slice_indices[slice_index]
def skip_path_check(self, bfp):
if os.path.basename(bfp) in self.excluded_paths:
def skip_path_check(self, bfp, excluded_paths, excluded_dirs):
if os.path.basename(bfp) in excluded_paths:
return True
if self.excluded_dirs:
if excluded_dirs:
assert bfp.startswith(self.args.src_test_dir)
bfp_relative = bfp[len(self.args.src_test_dir):].rstrip(os.sep)
if bfp_relative.startswith(*self.excluded_dirs):
if bfp_relative.startswith(*excluded_dirs):
return True
return False
def skip_open_link_path_check(self, bfp):
return self.skip_path_check(bfp, self.excluded_open_link_paths, self.excluded_open_link_dirs)
def skip_save_reload_path_check(self, bfp):
return self.skip_path_check(bfp, self.excluded_save_reload_paths, self.excluded_save_reload_dirs)
def invalid_path_exception_process(self, bfp, exception):
expected_failure = self.invalid_paths.get(os.path.basename(bfp), None)
if not expected_failure:
@@ -289,22 +338,45 @@ class TestBlendFileOpenAllTestFiles(TestHelper):
raise exception
print(f"\tExpected failure: '{exception}'", flush=True)
def save_reload(self, bfp, prefix):
if self.skip_save_reload_path_check(bfp):
return
tmp_save_path = os.path.join(self.args.output_dir, prefix + os.path.basename(bfp))
if not self.args.is_quiet:
print(f"Trying to save to {tmp_save_path}", flush=True)
bpy.ops.wm.save_as_mainfile(filepath=tmp_save_path, compress=True)
if not self.args.is_quiet:
print(f"Trying to reload from {tmp_save_path}", flush=True)
bpy.ops.wm.revert_mainfile()
if not self.args.is_quiet:
print(f"Removing {tmp_save_path}", flush=True)
bpy.ops.wm.read_homefile(use_empty=True, use_factory_startup=True)
# For some reasons, this can fail randomely... Juts ignore, this is only here to cleanup
# the hundreds of written files, not worth failing the test if it cannot be removed.
if os.path.exists(tmp_save_path):
os.remove(tmp_save_path)
# The 'backup' blendfile created when resaving over an exisitng one.
tmp_save_path_1 = tmp_save_path + "1"
if os.path.exists(tmp_save_path_1):
os.remove(tmp_save_path_1)
def test_open(self):
for bfp in self.blendfile_paths:
if self.skip_path_check(bfp):
if self.skip_open_link_path_check(bfp):
continue
if not self.args.is_quiet:
print(f"Trying to open {bfp}", flush=True)
bpy.ops.wm.read_homefile(use_empty=True, use_factory_startup=True)
try:
bpy.ops.wm.open_mainfile(filepath=bfp, load_ui=False)
self.save_reload(bfp, "OPENED_")
except BaseException as e:
self.invalid_path_exception_process(bfp, e)
def link_append(self, do_link):
operation_name = "link" if do_link else "append"
for bfp in self.blendfile_paths:
if self.skip_path_check(bfp):
if self.skip_open_link_path_check(bfp):
continue
bpy.ops.wm.read_homefile(use_empty=True, use_factory_startup=True)
try:
@@ -317,6 +389,7 @@ class TestBlendFileOpenAllTestFiles(TestHelper):
if not self.args.is_quiet:
print(f"Trying to {operation_name} {bfp}/Object/{lib_in.objects[0]}", flush=True)
lib_out.objects.append(lib_in.objects[0])
self.save_reload(bfp, f"{operation_name.upper()}_")
except BaseException as e:
self.invalid_path_exception_process(bfp, e)
@@ -328,7 +401,7 @@ class TestBlendFileOpenAllTestFiles(TestHelper):
TESTS = (
TestBlendFileOpenAllTestFiles,
TestBlendFileOpenLinkSaveAllTestFiles,
)
@@ -336,8 +409,8 @@ def argparse_create():
import argparse
# When --help or no args are given, print this help
description = ("Test basic versioning code by opening all blend files "
"in `tests/files` directory.")
description = ("Test basic versioning and writing code by opening, linking from, saving and reloading"
"all blend files in `--src-test-dir` directory (typically the `tests/files` one).")
parser = argparse.ArgumentParser(description=description)
parser.add_argument(
"--src-test-dir",
@@ -346,6 +419,13 @@ def argparse_create():
help="Root tests directory to search for blendfiles",
required=False,
)
parser.add_argument(
"--output-dir",
dest="output_dir",
default=".",
help="Where to output temp saved blendfiles",
required=False,
)
parser.add_argument(
"--quiet",