Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2025 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
add_blender_test(
assets_remote_library_listing_asset_catalogs_test
--python ${CMAKE_CURRENT_LIST_DIR}/listing_asset_catalogs_test.py
--
--outdir "${TEST_OUT_DIR}/assets"
)
add_blender_test(
assets_remote_library_listing_downloader_test
--python ${CMAKE_CURRENT_LIST_DIR}/listing_downloader_test.py
--
--outdir "${TEST_OUT_DIR}/assets"
)
add_blender_test(
assets_remote_library_listing_generator_test
--python ${CMAKE_CURRENT_LIST_DIR}/listing_generator_test.py
)

View File

@@ -0,0 +1,98 @@
# SPDX-FileCopyrightText: 2026 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import sys
import unittest
from pathlib import Path
from _bpy_internal.assets.remote_library import listing_asset_catalogs
from _bpy_internal.assets.remote_library import blender_asset_library_openapi as api_models
"""
blender -b --factory-startup -P tests/python/assets/remote_library/listing_asset_catalogs_test.py -- --outdir=/tmp
"""
# CLI argument, will be set to its actual value in main() below.
arg_outdir: Path
class ListingDownloaderTest(unittest.TestCase):
cats_path: Path
@classmethod
def setUpClass(cls) -> None:
arg_outdir.mkdir(parents=True, exist_ok=True)
cls.cats_path = arg_outdir / "test_catalogs.cats.txt"
def tearDown(self) -> None:
self.cats_path.unlink(missing_ok=True)
def test_write_sanitized_cats(self) -> None:
catalogs = [
api_models.CatalogV1(
path="Cats/Laksa",
uuids=["cdf49402-6814-5c20-a026-9f3211d8a615"],
simple_name="cats-laksa",
),
api_models.CatalogV1(
path="Cats/Quercus\nwith\nnewlines",
uuids=["2143dfac-9e81-5a31-99f2-befa8ff26ac2"],
simple_name="cats-quercus",
),
api_models.CatalogV1(
path="Cats/Raymond\n: Pawducer",
uuids=["db847984-cb61-5197-87b5-39134a2e758c"],
simple_name="cats-raymond",
),
api_models.CatalogV1(
path="Cats/Muesli: Huntress",
uuids=["d7ce44c8-d1df-5055-bae4-8a6ed03b8329"],
simple_name="cats-muesli",
),
]
asset_library_meta = api_models.AssetLibraryMeta(
api_versions={},
contact=api_models.Contact(name="Unit the Tester"),
name="Cats of\nAmsterdam",
)
listing_asset_catalogs.write(catalogs, self.cats_path, asset_library_meta)
# Check the file is as expected.
file_lines = self.cats_path.read_text().splitlines()
# The line numbers are determined by listing_asset_catalogs._ASSET_CATS_HEADER.
header_num_lines = len(listing_asset_catalogs._ASSET_CATS_HEADER.splitlines())
self.assertIn("Cats of Amsterdam", file_lines[header_num_lines - 3])
# The catalogs should be ordered by path, and sanitized.
written_cats = file_lines[header_num_lines + 1:]
expect_cats = [
"cdf49402-6814-5c20-a026-9f3211d8a615:Cats/Laksa:cats-laksa",
"d7ce44c8-d1df-5055-bae4-8a6ed03b8329:Cats/Muesli Huntress:cats-muesli",
"2143dfac-9e81-5a31-99f2-befa8ff26ac2:Cats/Quercus with newlines:cats-quercus",
"db847984-cb61-5197-87b5-39134a2e758c:Cats/Raymond Pawducer:cats-raymond",
]
self.assertEqual(expect_cats, written_cats)
def main():
global arg_outdir
import argparse
argv = [sys.argv[0]]
if '--' in sys.argv:
argv += sys.argv[sys.argv.index('--') + 1:]
parser = argparse.ArgumentParser()
parser.add_argument('--outdir', required=True, type=Path)
args, remaining = parser.parse_known_args(argv)
arg_outdir = args.outdir
unittest.main(argv=remaining)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,302 @@
# SPDX-FileCopyrightText: 2026 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import shutil
import sys
import unittest
from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath
from typing import override
from _bpy_internal.assets.remote_library import json_parsing, listing_downloader
from _bpy_internal.assets.remote_library import blender_asset_library_openapi as api_models
import bpy
"""
blender -b --factory-startup -P tests/python/assets/remote_library/listing_downloader_test.py -- --outdir=/tmp
"""
# CLI argument, will be set to its actual value in main() below.
arg_outdir: Path
class ListingDownloaderTest(unittest.TestCase):
json_path: Path
@classmethod
def setUpClass(cls) -> None:
arg_outdir.mkdir(parents=True, exist_ok=True)
cls.json_path = arg_outdir / "asset_page.json"
def tearDown(self) -> None:
self.json_path.unlink(missing_ok=True)
def test_sanitize_asset_page__bad_paths(self) -> None:
# These paths should be handled identically, independent of the current platform.
self._test_sanitize_asset_page_with_specific_bad_path(r"C:\temp\kubus.blend", "temp/kubus.blend")
self.assertTrue(self.json_path.exists(), "JSON file should have been rewritten")
self._test_sanitize_asset_page_with_specific_bad_path(r"C:/temp/kubus.blend", "temp/kubus.blend")
self.assertTrue(self.json_path.exists(), "JSON file should have been rewritten")
self._test_sanitize_asset_page_with_specific_bad_path("/temp/kubus.blend", "temp/kubus.blend")
self.assertTrue(self.json_path.exists(), "JSON file should have been rewritten")
self._test_sanitize_asset_page_with_specific_bad_path("//temp/kubus.blend", "temp/kubus.blend")
self.assertTrue(self.json_path.exists(), "JSON file should have been rewritten")
self._test_sanitize_asset_page_with_specific_bad_path(r"\\NAS\share\temp\kubus.blend", "temp/kubus.blend")
self.assertTrue(self.json_path.exists(), "JSON file should have been rewritten")
self._test_sanitize_asset_page_with_specific_bad_path("/temp/kubus.blend", "temp/kubus.blend")
self.assertTrue(self.json_path.exists(), "JSON file should have been rewritten")
self._test_sanitize_asset_page_with_specific_bad_path(r"\temp\kubus.blend", "temp/kubus.blend")
self.assertTrue(self.json_path.exists(), "JSON file should have been rewritten")
self._test_sanitize_asset_page_with_specific_bad_path(
r"//NAS/share/temp/kubus.blend", "NAS/share/temp/kubus.blend")
self.assertTrue(self.json_path.exists(), "JSON file should have been rewritten")
self._test_sanitize_asset_page_with_specific_bad_path(
r"//localhost\C$\Windows\System32\kubus.blend", "Windows/System32/kubus.blend")
self._test_sanitize_asset_page_with_specific_bad_path(
r"//localhost/C$/Windows/System32/kubus.blend", "localhost/C$/Windows/System32/kubus.blend")
self.assertTrue(self.json_path.exists(), "JSON file should have been rewritten")
# Relative path that attempts to break out of the asset library.
self._test_sanitize_asset_page_with_specific_bad_path(
"sneaky/path/../../../temp/kubus.blend", "temp/kubus.blend")
self.assertTrue(self.json_path.exists(), "JSON file should have been rewritten")
def test_sanitize_asset_page__all_ok(self) -> None:
# Construct an incorrect asset page, with bad counts and an absolute path to a file.
blend_path = "temp/kubus.blend"
asset_page = api_models.AssetLibraryIndexPageV1(
# Set correct asset & file counts.
asset_count=1,
file_count=1,
assets=[
api_models.AssetV1(
name="Kubus",
id_type="OBJECT",
files=[blend_path],
thumbnail=api_models.URLWithHash(url="thumbs/kubus.webp", hash="12345"),
meta=None,
bl_versions=api_models.AssetBlenderVersionsV1(min="2.0"),
),
],
files=[
api_models.FileV1(
# Absolute path, should get corrected.
path=blend_path,
size_in_bytes=328051946337,
hash="CAT:51756572637573",
blender_version="5.2",
url=None,
),
],
)
Downloader = listing_downloader.RemoteAssetListingDownloader
dl = Downloader(
remote_url="http://localhost/",
local_path="/tmp/does-not-matter-we-do-not-write",
on_update_callback=lambda downloader: None,
on_done_callback=lambda downloader: None,
on_metafiles_done_callback=None,
on_page_done_callback=None,
)
self.json_path.unlink(missing_ok=True)
dl._sanitize_asset_page(asset_page, self.json_path)
# Check that the counts have been kept the same.
self.assertEqual(1, asset_page.asset_count)
self.assertEqual(1, asset_page.file_count)
# Check that the file paths are also kept the same.
self.assertEqual(blend_path, asset_page.files[0].path, "In-memory file entry should have been sanitized")
self.assertEqual(
[blend_path],
asset_page.assets[0].files,
"In-memory file reference of asset entry should have been sanitized")
# Check that the JSON file does not exist, because rewriting was not necessary.
self.assertFalse(self.json_path.exists(), "JSON file should NOT have been rewritten")
def _test_sanitize_asset_page_with_specific_bad_path(self, bad_path: str, sanitized_path: str) -> None:
# Construct an incorrect asset page, with bad counts and an absolute path to a file.
asset_page = api_models.AssetLibraryIndexPageV1(
# Set incorrect asset & file counts. These should get corrected.
asset_count=47,
file_count=327,
assets=[
api_models.AssetV1(
name="Kubus",
id_type="OBJECT",
files=[bad_path],
thumbnail=api_models.URLWithHash(url="thumbs/kubus.webp", hash="12345"),
meta=None,
bl_versions=api_models.AssetBlenderVersionsV1(min="2.0"),
),
],
files=[
api_models.FileV1(
# Absolute path, should get corrected.
path=bad_path,
size_in_bytes=328051946337,
hash="CAT:51756572637573",
blender_version="5.2",
url=None,
),
],
)
Downloader = listing_downloader.RemoteAssetListingDownloader
dl = Downloader(
remote_url="http://localhost/",
local_path="/tmp/does-not-matter-we-do-not-write",
on_update_callback=lambda downloader: None,
on_done_callback=lambda downloader: None,
on_metafiles_done_callback=None,
on_page_done_callback=None,
)
self.json_path.unlink(missing_ok=True)
dl._sanitize_asset_page(asset_page, self.json_path)
# Check that the counts have been sanitized.
self.assertEqual(1, asset_page.asset_count)
self.assertEqual(1, asset_page.file_count)
# Check that the absolute file path has been sanitized.
# Both the Windows and the POSIX version of bad_path should sanitize to this path.
self.assertEqual(sanitized_path, asset_page.files[0].path, "In-memory file entry should have been sanitized")
self.assertEqual([sanitized_path], asset_page.assets[0].files,
"In-memory file reference of asset entry should have been sanitized")
# Check that the JSON file has been updated, so that when Blender later reads it, it's been sanitized.
parser = json_parsing.ValidatingParser()
json_payload = self.json_path.read_bytes()
from_file = parser.parse_and_validate(api_models.AssetLibraryIndexPageV1, json_payload)
# Check that the counts have been sanitized.
self.assertEqual(1, from_file.asset_count)
self.assertEqual(1, from_file.file_count)
# Check that the absolute file path has been sanitized.
# Both the Windows and the POSIX version of bad_path should sanitize to this path.
self.assertEqual(sanitized_path, from_file.files[0].path, "File entry should have been sanitized")
self.assertEqual([sanitized_path], from_file.assets[0].files,
"File reference of asset entry should have been sanitized")
class PathGuessingTest(unittest.TestCase):
def test_str_to_path_multiplatform(self) -> None:
str_to_path = listing_downloader._str_to_path_multiplatform
self.assertEqual(PureWindowsPath(r'C:\Program Files\Blender'), str_to_path(r'C:\Program Files/Blender'))
self.assertEqual(PureWindowsPath(r'C:\Program Files\Blender'), str_to_path(r'C:/Program Files/Blender'))
self.assertEqual(PureWindowsPath(r'\\NAS\share\flamenco\file.blend'),
str_to_path(r'\\NAS\share\flamenco\file.blend'))
self.assertEqual(PurePosixPath('/Program Files/Blender'), str_to_path('/Program Files/Blender'))
self.assertEqual(PurePath('file.blend'), str_to_path('file.blend'))
class SanitizePathFromURLTest(unittest.TestCase):
def test_sanitize_path_from_url(self) -> None:
_sanitize_path_from_url = listing_downloader._sanitize_path_from_url
self.assertEqual(
PurePosixPath('normal/path/as/expected.blend'),
_sanitize_path_from_url('/normal/path/as/expected.blend'),
)
self.assertEqual(
PurePosixPath('normal/path/as/expected.blend'),
_sanitize_path_from_url(PurePosixPath('/normal/path/as/expected.blend')),
)
self.assertEqual(
PurePosixPath('.'),
_sanitize_path_from_url(PurePosixPath('')),
)
self.assertEqual(
PurePosixPath('path/filename.blend'),
_sanitize_path_from_url(PurePosixPath('/path/sub/../filename.blend')),
)
self.assertEqual(
PurePosixPath('path/filename.blend'),
_sanitize_path_from_url('/path/sub%2F%2E%2e/filename.blend'),
)
self.assertEqual(
PurePosixPath('path/filename.blend'),
_sanitize_path_from_url('path/filename.blend'),
)
self.assertEqual(
PurePosixPath('longer/filename.blend'),
_sanitize_path_from_url(PurePosixPath('/longer/faster/path/../../filename.blend')),
)
self.assertEqual(
PurePosixPath('filename.blend'),
_sanitize_path_from_url(PurePosixPath('/faster/path/../../filename.blend')),
)
self.assertEqual(
PurePosixPath('filename.blend'),
_sanitize_path_from_url('/faster/path/../../filename.blend'),
)
self.assertEqual(
PurePosixPath('etc/passwd'),
_sanitize_path_from_url(PurePosixPath('/../../../../../etc/passwd')),
)
class RemoteAssetListingLocatorTest(unittest.TestCase):
asset_lib_path: Path
@override
def setUp(self) -> None:
arg_outdir.mkdir(parents=True, exist_ok=True)
self.asset_lib_path = arg_outdir / "test_asset_library"
@override
def tearDown(self) -> None:
if self.asset_lib_path.exists():
shutil.rmtree(self.asset_lib_path)
def test_is_system_path(self) -> None:
locator = listing_downloader.RemoteAssetListingLocator('https://example.com/', self.asset_lib_path)
self.assertFalse(locator.is_system_path(Path()))
self.assertFalse(locator.is_system_path(arg_outdir))
self.assertFalse(locator.is_system_path(self.asset_lib_path))
self.assertFalse(locator.is_system_path(self.asset_lib_path / "valid-download.blend"))
self.assertTrue(locator.is_system_path(self.asset_lib_path / "_asset-library-meta.json"))
self.assertTrue(locator.is_system_path(self.asset_lib_path / "_v1/asset-index.json"))
self.assertTrue(locator.is_system_path(self.asset_lib_path / "_v1/assets-00000.json"))
self.assertTrue(locator.is_system_path(self.asset_lib_path / "_v1/file-that-is-not-used.toml"))
self.assertTrue(locator.is_system_path(self.asset_lib_path / "_listing_backup"))
self.assertTrue(locator.is_system_path(self.asset_lib_path / "_listing_backup/whatever.txt"))
# Check the file hash path. It's a special case, because it's constructed from various parts and thus uses
# pattern matching.
self.assertTrue(locator.is_system_path(self.asset_lib_path / "_file_hashes_v1.sqlite"))
self.assertTrue(locator.is_system_path(self.asset_lib_path / "_FILE_hashes_v2.SqLiTe"))
self.assertFalse(locator.is_system_path(self.asset_lib_path / "harmless_file_hashes_v1.sqlite"))
# Test some sneaky indirection. This is why the resolve() function is called in is_listing_path().
self.assertTrue(locator.is_system_path(self.asset_lib_path / "../.." /
self.asset_lib_path.parts[-2] / self.asset_lib_path.parts[-1] / "_v1"))
def main():
global arg_outdir
import argparse
argv = [sys.argv[0]]
if '--' in sys.argv:
argv += sys.argv[sys.argv.index('--') + 1:]
parser = argparse.ArgumentParser()
parser.add_argument('--outdir', required=True, type=Path)
args, remaining = parser.parse_known_args(argv)
arg_outdir = args.outdir
unittest.main(argv=remaining)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,145 @@
# SPDX-FileCopyrightText: 2025 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import sys
import unittest
from _bpy_internal.assets.remote_library import json_parsing, hashing
from _bpy_internal.assets.remote_library import blender_asset_library_openapi as api_models
from _bpy_internal.assets.remote_library import cli_listing_generator_asset_finder as asset_finder
import bpy
"""
blender -b --factory-startup --python tests/python/assets/remote_library/listing_generator_test.py
"""
class CustomPropertiesTest(unittest.TestCase):
cube: bpy.types.Object
def setUp(self) -> None:
bpy.ops.wm.read_homefile(use_factory_startup=True)
self.cube = bpy.data.objects['Cube']
self.cube.asset_mark()
self.maxDiff = 100000
def test_empty_metadata(self) -> None:
del self.cube.asset_data["dimensions"]
meta = asset_finder._get_asset_meta(self.cube.asset_data)
self.assertIsNone(meta)
def test_plain_properties(self) -> None:
asset_data = self.cube.asset_data
asset_data["barcode"] = "155366" # Integer-like, should be stored as string.
asset_data["location"] = "café" # Non-ASCII string.
asset_data["size"] = 32.7 # FLOAT
asset_data["count"] = 47 # INT
asset_data["amazing"] = True # BOOL
meta = asset_finder._get_asset_meta(asset_data)
Types = api_models.CustomPropertyTypeV1
Prop = api_models.CustomPropertyV1
# autopep8: off
expected_props = [
Prop(name='dimensions', type=Types.IDP_ARRAY, value=[2.0, 2.0, 2.0], itemtype=Types.IDP_FLOAT),
Prop(name='barcode', type=Types.IDP_STRING, value='155366'),
Prop(name='location', type=Types.IDP_STRING, value='café'),
Prop(name='size', type=Types.IDP_FLOAT, value=32.7),
Prop(name='count', type=Types.IDP_INT, value=47),
Prop(name='amazing', type=Types.IDP_BOOL, value=True),
]
# autopep8: on
assert meta is not None
self.assertEqual(expected_props, meta.properties)
def test_array_properties(self) -> None:
asset_data = self.cube.asset_data
asset_data["agents"] = ["007", "47", "327"]
asset_data["locations"] = ["Hokkaido", "Santa Fortuna", "Sapienza"]
asset_data["boundingbox"] = [-3.0, -4.0, -0.1, 1, 2, 3]
meta = asset_finder._get_asset_meta(asset_data)
Types = api_models.CustomPropertyTypeV1
Prop = api_models.CustomPropertyV1
# autopep8: off
expected_prop = [
Prop(name='dimensions', type=Types.IDP_ARRAY, value=[2.0, 2.0, 2.0], itemtype=Types.IDP_FLOAT),
Prop(name='agents', type=Types.IDP_ARRAY, value=["007", "47", "327"], itemtype=Types.IDP_STRING),
Prop(name='locations', type=Types.IDP_ARRAY, value=["Hokkaido", "Santa Fortuna", "Sapienza"], itemtype=Types.IDP_STRING),
Prop(name='boundingbox', type=Types.IDP_ARRAY, value=[-3.0, -4.0, -0.1, 1.0, 2.0, 3.0], itemtype=Types.IDP_FLOAT),
]
# autopep8: on
assert meta is not None
self.assertEqual(expected_prop, meta.properties)
def test_serialize_to_json(self) -> None:
meta = asset_finder._get_asset_meta(self.cube.asset_data)
# The asset metadata should be convertable to JSON.
parser = json_parsing.ValidatingParser()
as_json = parser.dumps(meta)
self.assertIsNotNone(as_json)
# The JSON should also be deserializable as well, and produce the same data.
roundtripped = parser.parse_and_validate(api_models.AssetMetadataV1, as_json)
self.assertEqual(meta, roundtripped)
class HashingTest(unittest.TestCase):
def test_url_function(self) -> None:
# No hash.
url_with_hash = api_models.URLWithHash(
url="http://localhost:8080/_v1/asset-index.json",
hash=""
)
self.assertEqual("http://localhost:8080/_v1/asset-index.json", hashing.url(url_with_hash))
# Hash without type, and to-be-quoted characters.
url_with_hash.hash = "this is a weird häsh"
self.assertEqual(
"http://localhost:8080/_v1/asset-index.json?hash=this%20is%20a%20weird%20h%C3%A4sh",
hashing.url(url_with_hash))
# Hash with a type prefix, should be stripped.
url_with_hash.hash = "sha1:2cafc9d388fb8c2d0b6ca9780d6b75963587916d"
self.assertEqual(
"http://localhost:8080/_v1/asset-index.json?hash=2cafc9d388fb8c2d0b6ca9780d6b75963587916d",
hashing.url(url_with_hash))
# Existing query string, should be correctly appended to.
url_with_hash.url = "http://localhost:8080/_v1/asset-index.json?auth=none"
self.assertEqual(
"http://localhost:8080/_v1/asset-index.json?auth=none&hash=2cafc9d388fb8c2d0b6ca9780d6b75963587916d",
hashing.url(url_with_hash))
# Using a tuple instead of an URLWithHash object.
self.assertEqual(
"http://localhost:8080/_v1/asset-index.json?auth=none&hash=2cafc9d388fb8c2d0b6ca9780d6b75963587916d",
hashing.url((
"http://localhost:8080/_v1/asset-index.json?auth=none",
"sha1:2cafc9d388fb8c2d0b6ca9780d6b75963587916d"
)))
def main():
global args
argv = [sys.argv[0]]
if '--' in sys.argv:
argv += sys.argv[sys.argv.index('--') + 1:]
unittest.main(argv=argv)
if __name__ == "__main__":
main()