Blender V5.0
asset_catalog_test.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2020 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
5#include "AS_asset_catalog.hh"
9
10#include "BKE_preferences.h"
11
12#include "BLI_fileops.h"
13#include "BLI_path_utils.hh"
14
15#include "DNA_asset_types.h"
16#include "DNA_userdef_types.h"
17
18#include "testing/testing.h"
19
21
23
24/* UUIDs from tests/files/asset_library/blender_assets.cats.txt */
25const bUUID UUID_ID_WITHOUT_PATH("e34dd2c5-5d2e-4668-9794-1db5de2a4f71");
26const bUUID UUID_POSES_ELLIE("df60e1f6-2259-475b-93d9-69a1b4a8db78");
27const bUUID UUID_POSES_ELLIE_WHITESPACE("b06132f6-5687-4751-a6dd-392740eb3c46");
28const bUUID UUID_POSES_ELLIE_TRAILING_SLASH("3376b94b-a28d-4d05-86c1-bf30b937130d");
29const bUUID UUID_POSES_ELLIE_BACKSLASHES("a51e17ae-34fc-47d5-ba0f-64c2c9b771f7");
30const bUUID UUID_POSES_RUZENA("79a4f887-ab60-4bd4-94da-d572e27d6aed");
31const bUUID UUID_POSES_RUZENA_HAND("81811c31-1a88-4bd7-bb34-c6fc2607a12e");
32const bUUID UUID_POSES_RUZENA_FACE("82162c1f-06cc-4d91-a9bf-4f72c104e348");
33const bUUID UUID_WITHOUT_SIMPLENAME("d7916a31-6ca9-4909-955f-182ca2b81fa3");
34const bUUID UUID_ANOTHER_RUZENA("00000000-d9fa-4b91-b704-e6af1f1339ef");
35
36/* UUIDs from tests/files/asset_library/modified_assets.cats.txt */
37const bUUID UUID_AGENT_47("c5744ba5-43f5-4f73-8e52-010ad4a61b34");
38
39/* Subclass that adds accessors such that protected fields can be used in tests. */
41 public:
43
44 explicit TestableAssetCatalogService(const CatalogFilePath &asset_library_root)
45 : AssetCatalogService(asset_library_root)
46 {
47 }
48
53
58
63
68
70 {
71 int64_t count = 0;
72 for (const auto &catalog_uptr : get_catalogs().values()) {
73 if (catalog_uptr->path == path) {
74 count++;
75 }
76 }
77 return count;
78 }
79};
80
82 protected:
83 /* Used by on_blendfile_save__from_memory_into_existing_asset_lib* test functions. */
84 void save_from_memory_into_existing_asset_lib(const bool should_top_level_cdf_exist)
85 {
86 const CatalogFilePath target_dir = create_temp_path(); /* Has trailing slash. */
87 const CatalogFilePath original_cdf_file = asset_library_root_ + SEP_STR +
88 "blender_assets.cats.txt";
89 const CatalogFilePath registered_asset_lib = target_dir + "my_asset_library" + SEP_STR;
90 const CatalogFilePath asset_lib_subdir = registered_asset_lib + "subdir" + SEP_STR;
91 CatalogFilePath cdf_toplevel = registered_asset_lib +
93 CatalogFilePath cdf_in_subdir = asset_lib_subdir +
95 BLI_path_slash_native(cdf_toplevel.data());
96 BLI_path_slash_native(cdf_in_subdir.data());
97
98 /* Set up a temporary asset library for testing. */
100 &U, "Test", registered_asset_lib.c_str());
101 ASSERT_NE(nullptr, asset_lib_pref);
102 ASSERT_TRUE(BLI_dir_create_recursive(asset_lib_subdir.c_str()));
103
104 if (should_top_level_cdf_exist) {
105 ASSERT_EQ(0, BLI_copy(original_cdf_file.c_str(), cdf_toplevel.c_str()));
106 }
107
108 /* Create an empty CDF to add complexity. It should not save to this, but to the top-level
109 * one. */
110 ASSERT_TRUE(BLI_file_touch(cdf_in_subdir.c_str()));
111 ASSERT_EQ(0, BLI_file_size(cdf_in_subdir.c_str()));
112
113 /* Create the catalog service without loading the already-existing CDF. */
115 const CatalogFilePath blendfilename = asset_lib_subdir + "some_file.blend";
116 const AssetCatalog *cat = service.create_catalog("some/catalog/path");
117
118 /* Mock that the blend file is written to the directory already containing a CDF. */
119 ASSERT_TRUE(service.write_to_disk(blendfilename));
120
121 /* Test that the CDF still exists in the expected location. */
122 EXPECT_TRUE(BLI_exists(cdf_toplevel.c_str()));
123 const CatalogFilePath backup_filename = cdf_toplevel + "~";
124 const bool backup_exists = BLI_exists(backup_filename.c_str());
125 EXPECT_EQ(should_top_level_cdf_exist, backup_exists)
126 << "Overwritten CDF should have been backed up.";
127
128 /* Test that the in-memory CDF has the expected file path. */
130 std::string native_cdf_path = cdf->file_path;
131 BLI_path_slash_native(native_cdf_path.data());
132 EXPECT_EQ(cdf_toplevel, native_cdf_path);
133
134 /* Test that the in-memory catalogs have been merged with the on-disk one. */
135 AssetCatalogService loaded_service(cdf_toplevel);
136 loaded_service.load_from_disk();
137 EXPECT_NE(nullptr, loaded_service.find_catalog(cat->catalog_id));
138
139 /* This catalog comes from a pre-existing CDF that should have been merged.
140 * However, if the file doesn't exist, so does the catalog. */
141 AssetCatalog *poses_ellie_catalog = loaded_service.find_catalog(UUID_POSES_ELLIE);
142 if (should_top_level_cdf_exist) {
143 EXPECT_NE(nullptr, poses_ellie_catalog);
144 }
145 else {
146 EXPECT_EQ(nullptr, poses_ellie_catalog);
147 }
148
149 /* Test that the "red herring" CDF has not been touched. */
150 EXPECT_EQ(0, BLI_file_size(cdf_in_subdir.c_str()));
151
152 BKE_preferences_asset_library_remove(&U, asset_lib_pref);
153 }
154};
155
156TEST_F(AssetCatalogTest, load_single_file)
157{
158 AssetCatalogService service(asset_library_root_);
159 service.load_from_disk(asset_library_root_ + SEP_STR + "blender_assets.cats.txt");
160
161 /* Test getting a non-existent catalog ID. */
163
164 /* Test getting an invalid catalog (without path definition). */
165 AssetCatalog *cat_without_path = service.find_catalog(UUID_ID_WITHOUT_PATH);
166 ASSERT_EQ(nullptr, cat_without_path);
167
168 /* Test getting a regular catalog. */
169 AssetCatalog *poses_ellie = service.find_catalog(UUID_POSES_ELLIE);
170 ASSERT_NE(nullptr, poses_ellie);
172 EXPECT_EQ("character/Ellie/poselib", poses_ellie->path.str());
173 EXPECT_EQ("POSES_ELLIE", poses_ellie->simple_name);
174
175 /* Test white-space stripping and support in the path. */
176 AssetCatalog *poses_whitespace = service.find_catalog(UUID_POSES_ELLIE_WHITESPACE);
177 ASSERT_NE(nullptr, poses_whitespace);
179 EXPECT_EQ("character/Ellie/poselib/white space", poses_whitespace->path.str());
180 EXPECT_EQ("POSES_ELLIE WHITESPACE", poses_whitespace->simple_name);
181
182 /* Test getting a UTF8 catalog ID. */
183 AssetCatalog *poses_ruzena = service.find_catalog(UUID_POSES_RUZENA);
184 ASSERT_NE(nullptr, poses_ruzena);
185 EXPECT_EQ(UUID_POSES_RUZENA, poses_ruzena->catalog_id);
186 EXPECT_EQ("character/Ružena/poselib", poses_ruzena->path.str());
187 EXPECT_EQ("POSES_RUŽENA", poses_ruzena->simple_name);
188
189 /* Test getting a catalog that aliases an earlier-defined catalog. */
190 AssetCatalog *another_ruzena = service.find_catalog(UUID_ANOTHER_RUZENA);
191 ASSERT_NE(nullptr, another_ruzena);
192 EXPECT_EQ(UUID_ANOTHER_RUZENA, another_ruzena->catalog_id);
193 EXPECT_EQ("character/Ružena/poselib", another_ruzena->path.str());
194 EXPECT_EQ("Another Ružena", another_ruzena->simple_name);
195}
196
197TEST_F(AssetCatalogTest, load_catalog_path_backslashes)
198{
199 AssetCatalogService service(asset_library_root_);
200 service.load_from_disk(asset_library_root_ + SEP_STR + "blender_assets.cats.txt");
201
202 const AssetCatalog *found_by_id = service.find_catalog(UUID_POSES_ELLIE_BACKSLASHES);
203 ASSERT_NE(nullptr, found_by_id);
204 EXPECT_EQ(AssetCatalogPath("character/Ellie/backslashes"), found_by_id->path)
205 << "Backslashes should be normalized when loading from disk.";
206 EXPECT_EQ(StringRefNull("Windows For Life!"), found_by_id->simple_name);
207
208 const AssetCatalog *found_by_path = service.find_catalog_by_path("character/Ellie/backslashes");
209 EXPECT_EQ(found_by_id, found_by_path)
210 << "Catalog with backslashed path should be findable by the normalized path.";
211
212 EXPECT_EQ(nullptr, service.find_catalog_by_path("character\\Ellie\\backslashes"))
213 << "Nothing should be found when searching for backslashes.";
214}
215
216TEST_F(AssetCatalogTest, is_first_loaded_flag)
217{
218 AssetCatalogService service(asset_library_root_);
219 service.load_from_disk(asset_library_root_ + SEP_STR + "blender_assets.cats.txt");
220
221 AssetCatalog *new_cat = service.create_catalog("never/before/seen/path");
222 EXPECT_FALSE(new_cat->flags.is_first_loaded)
223 << "Adding a catalog at runtime should never mark it as 'first loaded'; "
224 "only loading from disk is allowed to do that.";
225
226 AssetCatalog *alias_cat = service.create_catalog("character/Ružena/poselib");
227 EXPECT_FALSE(alias_cat->flags.is_first_loaded)
228 << "Adding a new catalog with an already-loaded path should not mark it as 'first loaded'";
229
230 EXPECT_TRUE(service.find_catalog(UUID_POSES_ELLIE)->flags.is_first_loaded);
234
235 AssetCatalog *ruzena = service.find_catalog_by_path("character/Ružena/poselib");
237 << "The first-seen definition of a catalog should be returned";
238}
239
240TEST_F(AssetCatalogTest, find_catalog_by_path)
241{
242 TestableAssetCatalogService service(asset_library_root_);
243 service.load_from_disk(asset_library_root_ + SEP_STR +
245
246 AssetCatalog *catalog;
247
248 EXPECT_EQ(nullptr, service.find_catalog_by_path(""));
249 catalog = service.find_catalog_by_path("character/Ellie/poselib/white space");
250 EXPECT_NE(nullptr, catalog);
252 catalog = service.find_catalog_by_path("character/Ružena/poselib");
253 EXPECT_NE(nullptr, catalog);
255
256 /* "character/Ellie/poselib" is used by two catalogs. Check if it's using the first one. */
257 catalog = service.find_catalog_by_path("character/Ellie/poselib");
258 EXPECT_NE(nullptr, catalog);
260 EXPECT_NE(UUID_POSES_ELLIE_TRAILING_SLASH, catalog->catalog_id);
261}
262
263TEST_F(AssetCatalogTest, write_single_file)
264{
265 TestableAssetCatalogService service(asset_library_root_);
266 service.load_from_disk(asset_library_root_ + SEP_STR +
268
269 const CatalogFilePath save_to_path = use_temp_path() +
272 cdf->write_to_disk(save_to_path);
273
274 AssetCatalogService loaded_service(save_to_path);
275 loaded_service.load_from_disk();
276
277 /* Test that the expected catalogs are there. */
278 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
279 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
280 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_TRAILING_SLASH));
281 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA));
282 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_HAND));
283 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_FACE));
284
285 /* Test that the invalid catalog definition wasn't copied. */
286 EXPECT_EQ(nullptr, loaded_service.find_catalog(UUID_ID_WITHOUT_PATH));
287
288 /* TODO(@sybren): test ordering of catalogs in the file. */
289}
290
291TEST_F(AssetCatalogTest, read_write_unicode_filepath)
292{
293 TestableAssetCatalogService service(asset_library_root_);
294 const CatalogFilePath load_from_path = asset_library_root_ + SEP_STR + "новый" + SEP_STR +
296 service.load_from_disk(load_from_path);
297
298 const CatalogFilePath save_to_path = use_temp_path() + "новый.cats.txt";
300 ASSERT_NE(nullptr, cdf) << "unable to load " << load_from_path;
301 EXPECT_TRUE(cdf->write_to_disk(save_to_path));
302
303 AssetCatalogService loaded_service(save_to_path);
304 loaded_service.load_from_disk();
305
306 /* Test that the file was loaded correctly. */
307 const bUUID materials_uuid("a2151dff-dead-4f29-b6bc-b2c7d6cccdb4");
308 const AssetCatalog *cat = loaded_service.find_catalog(materials_uuid);
309 ASSERT_NE(nullptr, cat);
310 EXPECT_EQ(materials_uuid, cat->catalog_id);
311 EXPECT_EQ(AssetCatalogPath("Материалы"), cat->path);
312 EXPECT_EQ("Russian Materials", cat->simple_name);
313}
314
315TEST_F(AssetCatalogTest, no_writing_empty_files)
316{
317 const CatalogFilePath temp_lib_root = create_temp_path();
318 AssetCatalogService service(temp_lib_root);
319 service.write_to_disk(temp_lib_root + "phony.blend");
320
321 const CatalogFilePath default_cdf_path = temp_lib_root +
323 EXPECT_FALSE(BLI_exists(default_cdf_path.c_str()));
324}
325
326/* Already loaded a CDF, saving to some unrelated directory. */
327TEST_F(AssetCatalogTest, on_blendfile_save__with_existing_cdf)
328{
329 const CatalogFilePath top_level_dir = create_temp_path(); /* Has trailing slash. */
330
331 /* Create a copy of the CDF in SVN, so we can safely write to it. */
332 const CatalogFilePath original_cdf_file = asset_library_root_ + SEP_STR +
333 "blender_assets.cats.txt";
334 const CatalogFilePath cdf_dirname = top_level_dir + "other_dir" + SEP_STR;
335 const CatalogFilePath cdf_filename = cdf_dirname + AssetCatalogService::DEFAULT_CATALOG_FILENAME;
336 ASSERT_TRUE(BLI_dir_create_recursive(cdf_dirname.c_str()));
337 ASSERT_EQ(0, BLI_copy(original_cdf_file.c_str(), cdf_filename.c_str()))
338 << "Unable to copy " << original_cdf_file << " to " << cdf_filename;
339
340 /* Load the CDF, add a catalog, and trigger a write. This should write to the loaded CDF. */
341 TestableAssetCatalogService service(cdf_filename);
342 service.load_from_disk();
343 const AssetCatalog *cat = service.create_catalog("some/catalog/path");
344 service.tag_has_unsaved_changes();
345
346 const CatalogFilePath blendfilename = top_level_dir + "subdir" + SEP_STR + "some_file.blend";
347 ASSERT_TRUE(service.write_to_disk(blendfilename));
348 EXPECT_EQ(cdf_filename, service.get_catalog_definition_file()->file_path);
349
350 /* Test that the CDF was created in the expected location. */
351 const CatalogFilePath backup_filename = cdf_filename + "~";
352 EXPECT_TRUE(BLI_exists(cdf_filename.c_str()));
353 EXPECT_TRUE(BLI_exists(backup_filename.c_str()))
354 << "Overwritten CDF should have been backed up.";
355
356 /* Test that the on-disk CDF contains the expected catalogs. */
357 AssetCatalogService loaded_service(cdf_filename);
358 loaded_service.load_from_disk();
359 EXPECT_NE(nullptr, loaded_service.find_catalog(cat->catalog_id))
360 << "Expected to see the newly-created catalog.";
361 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE))
362 << "Expected to see the already-existing catalog.";
363}
364
365/* Create some catalogs in memory, save to directory that doesn't contain anything else. */
366TEST_F(AssetCatalogTest, on_blendfile_save__from_memory_into_empty_directory)
367{
368 const CatalogFilePath target_dir = create_temp_path(); /* Has trailing slash. */
369
371 const AssetCatalog *cat = service.create_catalog("some/catalog/path");
372
373 const CatalogFilePath blendfilename = target_dir + "some_file.blend";
374 ASSERT_TRUE(service.write_to_disk(blendfilename));
375
376 /* Test that the CDF was created in the expected location. */
377 const CatalogFilePath expected_cdf_path = target_dir +
379 EXPECT_TRUE(BLI_exists(expected_cdf_path.c_str()));
380
381 /* Test that the in-memory CDF has been created, and contains the expected catalog. */
383 ASSERT_NE(nullptr, cdf);
384 EXPECT_TRUE(cdf->contains(cat->catalog_id));
385
386 /* Test that the on-disk CDF contains the expected catalog. */
387 AssetCatalogService loaded_service(expected_cdf_path);
388 loaded_service.load_from_disk();
389 EXPECT_NE(nullptr, loaded_service.find_catalog(cat->catalog_id));
390}
391
392/* Create some catalogs in memory, save to directory that contains a default CDF. */
393TEST_F(AssetCatalogTest, on_blendfile_save__from_memory_into_existing_cdf_and_merge)
394{
395 const CatalogFilePath target_dir = create_temp_path(); /* Has trailing slash. */
396 const CatalogFilePath original_cdf_file = asset_library_root_ + SEP_STR +
397 "blender_assets.cats.txt";
398 CatalogFilePath writable_cdf_file = target_dir + AssetCatalogService::DEFAULT_CATALOG_FILENAME;
399 BLI_path_slash_native(writable_cdf_file.data());
400 ASSERT_EQ(0, BLI_copy(original_cdf_file.c_str(), writable_cdf_file.c_str()));
401
402 /* Create the catalog service without loading the already-existing CDF. */
404 const AssetCatalog *cat = service.create_catalog("some/catalog/path");
405
406 /* Mock that the blend file is written to a subdirectory of the asset library. */
407 const CatalogFilePath blendfilename = target_dir + "some_file.blend";
408 ASSERT_TRUE(service.write_to_disk(blendfilename));
409
410 /* Test that the CDF still exists in the expected location. */
411 const CatalogFilePath backup_filename = writable_cdf_file + "~";
412 EXPECT_TRUE(BLI_exists(writable_cdf_file.c_str()));
413 EXPECT_TRUE(BLI_exists(backup_filename.c_str()))
414 << "Overwritten CDF should have been backed up.";
415
416 /* Test that the in-memory CDF has the expected file path. */
418 ASSERT_NE(nullptr, cdf);
419 EXPECT_EQ(writable_cdf_file, cdf->file_path);
420
421 /* Test that the in-memory catalogs have been merged with the on-disk one. */
422 AssetCatalogService loaded_service(writable_cdf_file);
423 loaded_service.load_from_disk();
424 EXPECT_NE(nullptr, loaded_service.find_catalog(cat->catalog_id));
425 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
426}
427
428/* Create some catalogs in memory, save to subdirectory of a registered asset library, where the
429 * subdirectory also contains a CDF. This should still write to the top-level dir of the asset
430 * library. */
432 on_blendfile_save__from_memory_into_existing_asset_lib_without_top_level_cdf)
433{
434 save_from_memory_into_existing_asset_lib(true);
435}
436
437/* Create some catalogs in memory, save to subdirectory of a registered asset library, where the
438 * subdirectory contains a CDF, but the top-level directory does not. This should still write to
439 * the top-level dir of the asset library. */
440TEST_F(AssetCatalogTest, on_blendfile_save__from_memory_into_existing_asset_lib)
441{
442 save_from_memory_into_existing_asset_lib(false);
443}
444
445TEST_F(AssetCatalogTest, create_first_catalog_from_scratch)
446{
447 /* Even from scratch a root directory should be known. */
448 const CatalogFilePath temp_lib_root = use_temp_path();
449 AssetCatalogService service;
450
451 /* Just creating the service should NOT create the path. */
452 EXPECT_FALSE(BLI_exists(temp_lib_root.c_str()));
453
454 AssetCatalog *cat = service.create_catalog("some/catalog/path");
455 ASSERT_NE(nullptr, cat);
456 EXPECT_EQ(cat->path, "some/catalog/path");
457 EXPECT_EQ(cat->simple_name, "some-catalog-path");
458
459 /* Creating a new catalog should not save anything to disk yet. */
460 EXPECT_FALSE(BLI_exists(temp_lib_root.c_str()));
461
462 /* Creating a new catalog should not mark the asset service as 'dirty'; that's
463 * the caller's responsibility. */
464 EXPECT_FALSE(service.has_unsaved_changes());
465
466 /* Writing to disk should create the directory + the default file. */
467 service.write_to_disk(temp_lib_root + "phony.blend");
468 EXPECT_TRUE(BLI_is_dir(temp_lib_root.c_str()));
469
470 const CatalogFilePath definition_file_path = temp_lib_root + SEP_STR +
472 EXPECT_TRUE(BLI_is_file(definition_file_path.c_str()));
473
474 AssetCatalogService loaded_service(temp_lib_root);
475 loaded_service.load_from_disk();
476
477 /* Test that the expected catalog is there. */
478 AssetCatalog *written_cat = loaded_service.find_catalog(cat->catalog_id);
479 ASSERT_NE(nullptr, written_cat);
480 EXPECT_EQ(written_cat->catalog_id, cat->catalog_id);
481 EXPECT_EQ(written_cat->path, cat->path.str());
482}
483
484TEST_F(AssetCatalogTest, create_catalog_after_loading_file)
485{
486 const CatalogFilePath temp_lib_root = create_temp_path();
487
488 /* Copy the asset catalog definition files to a separate location, so that we can test without
489 * overwriting the test file in SVN. */
490 const CatalogFilePath default_catalog_path = asset_library_root_ + SEP_STR +
492 const CatalogFilePath writable_catalog_path = temp_lib_root +
494 ASSERT_EQ(0, BLI_copy(default_catalog_path.c_str(), writable_catalog_path.c_str()));
495 EXPECT_TRUE(BLI_is_dir(temp_lib_root.c_str()));
496 EXPECT_TRUE(BLI_is_file(writable_catalog_path.c_str()));
497
498 TestableAssetCatalogService service(temp_lib_root);
499 service.load_from_disk();
500 EXPECT_EQ(writable_catalog_path, service.get_catalog_definition_file()->file_path);
501 EXPECT_NE(nullptr, service.find_catalog(UUID_POSES_ELLIE)) << "expected catalogs to be loaded";
502
503 /* This should create a new catalog but not write to disk. */
504 const AssetCatalog *new_catalog = service.create_catalog("new/catalog");
505 const bUUID new_catalog_id = new_catalog->catalog_id;
506 service.tag_has_unsaved_changes();
507
508 /* Reload the on-disk catalog file. */
509 TestableAssetCatalogService loaded_service(temp_lib_root);
510 loaded_service.load_from_disk();
511 EXPECT_EQ(writable_catalog_path, loaded_service.get_catalog_definition_file()->file_path);
512
513 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE))
514 << "expected pre-existing catalogs to be kept in the file";
515 EXPECT_EQ(nullptr, loaded_service.find_catalog(new_catalog_id))
516 << "expecting newly added catalog to not yet be saved to " << temp_lib_root;
517
518 /* Write and reload the catalog file. */
519 service.write_to_disk(temp_lib_root + "phony.blend");
520 AssetCatalogService reloaded_service(temp_lib_root);
521 reloaded_service.load_from_disk();
522 EXPECT_NE(nullptr, reloaded_service.find_catalog(UUID_POSES_ELLIE))
523 << "expected pre-existing catalogs to be kept in the file";
524 EXPECT_NE(nullptr, reloaded_service.find_catalog(new_catalog_id))
525 << "expecting newly added catalog to exist in the file";
526}
527
528TEST_F(AssetCatalogTest, create_catalog_path_cleanup)
529{
530 AssetCatalogService service;
531 AssetCatalog *cat = service.create_catalog(" /some/path / ");
532
533 EXPECT_FALSE(BLI_uuid_is_nil(cat->catalog_id));
534 EXPECT_EQ("some/path", cat->path.str());
535 EXPECT_EQ("some-path", cat->simple_name);
536}
537
538TEST_F(AssetCatalogTest, create_catalog_simple_name)
539{
540 AssetCatalogService service;
541 AssetCatalog *cat = service.create_catalog(
542 "production/Spite Fright/Characters/Victora/Pose Library/Approved/Body Parts/Hands");
543
544 EXPECT_FALSE(BLI_uuid_is_nil(cat->catalog_id));
545 EXPECT_EQ("production/Spite Fright/Characters/Victora/Pose Library/Approved/Body Parts/Hands",
546 cat->path.str());
547 EXPECT_EQ("...ht-Characters-Victora-Pose Library-Approved-Body Parts-Hands", cat->simple_name);
548}
549
550TEST_F(AssetCatalogTest, delete_catalog_leaf)
551{
552 AssetCatalogService service(asset_library_root_);
553 service.load_from_disk(asset_library_root_ + SEP_STR + "blender_assets.cats.txt");
554
555 /* Delete a leaf catalog, i.e. one that is not a parent of another catalog.
556 * This keeps this particular test easy. */
559
560 /* Contains not only paths from the CDF but also the missing parents (implicitly defined
561 * catalogs). This is why a leaf catalog was deleted. */
562 std::vector<AssetCatalogPath> expected_paths{
563 "character",
564 "character/Ellie",
565 "character/Ellie/backslashes",
566 "character/Ellie/poselib",
567 "character/Ellie/poselib/tailslash",
568 "character/Ellie/poselib/white space",
569 "character/Ružena",
570 "character/Ružena/poselib",
571 "character/Ružena/poselib/face",
572 // "character/Ružena/poselib/hand", /* This is the deleted one. */
573 "path",
574 "path/without",
575 "path/without/simplename",
576 };
577
578 const AssetCatalogTree &tree = service.catalog_tree();
580}
581
582TEST_F(AssetCatalogTest, delete_catalog_parent_by_id)
583{
584 TestableAssetCatalogService service(asset_library_root_);
585 service.load_from_disk(asset_library_root_ + SEP_STR + "blender_assets.cats.txt");
586
587 /* Delete a parent catalog. */
589
590 /* The catalog should have been deleted, but its children should still be there. */
591 EXPECT_EQ(nullptr, service.find_catalog(UUID_POSES_RUZENA));
592 EXPECT_NE(nullptr, service.find_catalog(UUID_POSES_RUZENA_FACE));
593 EXPECT_NE(nullptr, service.find_catalog(UUID_POSES_RUZENA_HAND));
594}
595
596TEST_F(AssetCatalogTest, delete_catalog_parent_by_path)
597{
598 AssetCatalogService service(asset_library_root_);
599 service.load_from_disk(asset_library_root_ + SEP_STR + "blender_assets.cats.txt");
600
601 /* Create an extra catalog with the to-be-deleted path, and one with a child of that.
602 * This creates some duplicates that are bound to occur in production asset libraries as well.
603 */
604 const bUUID cat1_uuid = service.create_catalog("character/Ružena/poselib")->catalog_id;
605 const bUUID cat2_uuid = service.create_catalog("character/Ružena/poselib/body")->catalog_id;
606
607 /* Delete a parent catalog. */
608 service.prune_catalogs_by_path("character/Ružena/poselib");
609
610 /* The catalogs and their children should have been deleted. */
611 EXPECT_EQ(nullptr, service.find_catalog(UUID_POSES_RUZENA));
614 EXPECT_EQ(nullptr, service.find_catalog(cat1_uuid));
615 EXPECT_EQ(nullptr, service.find_catalog(cat2_uuid));
616
617 /* Contains not only paths from the CDF but also the missing parents (implicitly defined
618 * catalogs). This is why a leaf catalog was deleted. */
619 std::vector<AssetCatalogPath> expected_paths{
620 "character",
621 "character/Ellie",
622 "character/Ellie/backslashes",
623 "character/Ellie/poselib",
624 "character/Ellie/poselib/tailslash",
625 "character/Ellie/poselib/white space",
626 "character/Ružena",
627 "path",
628 "path/without",
629 "path/without/simplename",
630 };
631
632 const AssetCatalogTree &tree = service.catalog_tree();
634}
635
636TEST_F(AssetCatalogTest, delete_catalog_write_to_disk)
637{
638 TestableAssetCatalogService service(asset_library_root_);
639 service.load_from_disk(asset_library_root_ + SEP_STR +
641
643
644 const CatalogFilePath save_to_path = use_temp_path();
647
648 AssetCatalogService loaded_service(save_to_path);
649 loaded_service.load_from_disk();
650
651 /* Test that the expected catalogs are there, except the deleted one. */
652 EXPECT_EQ(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
653 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
654 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_TRAILING_SLASH));
655 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA));
656 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_HAND));
657 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_FACE));
658}
659
660TEST_F(AssetCatalogTest, update_catalog_path)
661{
662 AssetCatalogService service(asset_library_root_);
663 service.load_from_disk(asset_library_root_ + SEP_STR +
665
666 const AssetCatalog *orig_cat = service.find_catalog(UUID_POSES_RUZENA);
667 const AssetCatalogPath orig_path = orig_cat->path;
668
669 service.update_catalog_path(UUID_POSES_RUZENA, "charlib/Ružena");
670
671 EXPECT_EQ(nullptr, service.find_catalog_by_path(orig_path))
672 << "The original (pre-rename) path should not be associated with a catalog any more.";
673
674 const AssetCatalog *renamed_cat = service.find_catalog(UUID_POSES_RUZENA);
675 ASSERT_NE(nullptr, renamed_cat);
676 ASSERT_EQ(orig_cat, renamed_cat) << "Changing the path should not reallocate the catalog.";
677 EXPECT_EQ(orig_cat->catalog_id, renamed_cat->catalog_id)
678 << "Changing the path should not change the catalog ID.";
679
680 EXPECT_EQ("charlib/Ružena", renamed_cat->path.str())
681 << "Changing the path should change the path. Surprise.";
682
683 EXPECT_EQ("charlib/Ružena/hand", service.find_catalog(UUID_POSES_RUZENA_HAND)->path.str())
684 << "Changing the path should update children.";
685 EXPECT_EQ("charlib/Ružena/face", service.find_catalog(UUID_POSES_RUZENA_FACE)->path.str())
686 << "Changing the path should update children.";
687}
688
689TEST_F(AssetCatalogTest, update_catalog_path_simple_name)
690{
691 AssetCatalogService service(asset_library_root_);
692 service.load_from_disk(asset_library_root_ + SEP_STR +
694 service.update_catalog_path(UUID_POSES_RUZENA, "charlib/Ružena");
695
696 /* This may not be valid forever; maybe at some point we'll expose the simple name to users &
697 * let them change it from the UI. Until then, automatically updating it is better, because
698 * otherwise all simple names would be "Catalog". */
699 EXPECT_EQ("charlib-Ružena", service.find_catalog(UUID_POSES_RUZENA)->simple_name)
700 << "Changing the path should update the simplename.";
701 EXPECT_EQ("charlib-Ružena-face", service.find_catalog(UUID_POSES_RUZENA_FACE)->simple_name)
702 << "Changing the path should update the simplename of children.";
703}
704
705TEST_F(AssetCatalogTest, update_catalog_path_longer_than_simplename)
706{
707 AssetCatalogService service(asset_library_root_);
708 service.load_from_disk(asset_library_root_ + SEP_STR +
710 const std::string new_path =
711 "this/is/a/very/long/path/that/exceeds/the/simple-name/length/of/assets";
712 ASSERT_GT(new_path.length(), sizeof(AssetMetaData::catalog_simple_name))
713 << "This test case should work with paths longer than AssetMetaData::catalog_simple_name";
714
715 service.update_catalog_path(UUID_POSES_RUZENA, new_path);
716
717 const std::string new_simple_name = service.find_catalog(UUID_POSES_RUZENA)->simple_name;
718 EXPECT_LT(new_simple_name.length(), sizeof(AssetMetaData::catalog_simple_name))
719 << "The new simple name should fit in the asset metadata.";
720 EXPECT_EQ("...very-long-path-that-exceeds-the-simple-name-length-of-assets", new_simple_name)
721 << "Changing the path should update the simplename.";
722 EXPECT_EQ("...long-path-that-exceeds-the-simple-name-length-of-assets-face",
724 << "Changing the path should update the simplename of children.";
725}
726
727TEST_F(AssetCatalogTest, update_catalog_path_add_slashes)
728{
729 AssetCatalogService service(asset_library_root_);
730 service.load_from_disk(asset_library_root_ + SEP_STR +
732
733 const AssetCatalog *orig_cat = service.find_catalog(UUID_POSES_RUZENA);
734 const AssetCatalogPath orig_path = orig_cat->path;
735
736 /* Original path is `character/Ružena/poselib`.
737 * This rename will also create a new catalog for `character/Ružena/poses`. */
738 service.update_catalog_path(UUID_POSES_RUZENA, "character/Ružena/poses/general");
739
740 EXPECT_EQ(nullptr, service.find_catalog_by_path(orig_path))
741 << "The original (pre-rename) path should not be associated with a catalog any more.";
742
743 const AssetCatalog *renamed_cat = service.find_catalog(UUID_POSES_RUZENA);
744 ASSERT_NE(nullptr, renamed_cat);
745 EXPECT_EQ(orig_cat->catalog_id, renamed_cat->catalog_id)
746 << "Changing the path should not change the catalog ID.";
747
748 EXPECT_EQ("character/Ružena/poses/general", renamed_cat->path.str())
749 << "When creating a new catalog by renaming + adding a slash, the renamed catalog should be "
750 "assigned the path passed to update_catalog_path()";
751
752 /* Test the newly created catalog. */
753 const AssetCatalog *new_cat = service.find_catalog_by_path("character/Ružena/poses");
754 ASSERT_NE(nullptr, new_cat) << "Renaming to .../X/Y should cause .../X to exist as well.";
755 EXPECT_EQ("character/Ružena/poses", new_cat->path.str());
756 EXPECT_EQ("character-Ružena-poses", new_cat->simple_name);
757 EXPECT_TRUE(new_cat->flags.has_unsaved_changes);
758
759 /* Test the children. */
760 EXPECT_EQ("character/Ružena/poses/general/hand",
762 << "Changing the path should update children.";
763 EXPECT_EQ("character/Ružena/poses/general/face",
765 << "Changing the path should update children.";
766}
767
768TEST_F(AssetCatalogTest, merge_catalog_files)
769{
770 const CatalogFilePath cdf_dir = create_temp_path();
771 const CatalogFilePath original_cdf_file = asset_library_root_ + SEP_STR +
772 "blender_assets.cats.txt";
773 const CatalogFilePath modified_cdf_file = asset_library_root_ + SEP_STR +
774 "modified_assets.cats.txt";
775 const CatalogFilePath temp_cdf_file = cdf_dir + "blender_assets.cats.txt";
776 ASSERT_EQ(0, BLI_copy(original_cdf_file.c_str(), temp_cdf_file.c_str()));
777
778 /* Load the unmodified, original CDF. */
779 TestableAssetCatalogService service(asset_library_root_);
780 service.load_from_disk(cdf_dir);
781
782 /* Copy a modified file, to mimic a situation where someone changed the
783 * CDF after we loaded it. */
784 ASSERT_EQ(0, BLI_copy(modified_cdf_file.c_str(), temp_cdf_file.c_str()));
785
786 /* Overwrite the modified file. This should merge the on-disk file with our catalogs.
787 * No catalog was marked as "has unsaved changes", so effectively this should not
788 * save anything, and reload what's on disk. */
789 service.write_to_disk(cdf_dir + "phony.blend");
790
791 AssetCatalogService loaded_service(cdf_dir);
792 loaded_service.load_from_disk();
793
794 /* Test that the expected catalogs are there. */
795 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
796 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_FACE));
797 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_AGENT_47)); /* New in the modified file. */
798
799 /* Test that catalogs removed from modified CDF are gone. */
800 EXPECT_EQ(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
802 EXPECT_EQ(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA));
803 EXPECT_EQ(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_HAND));
804
805 /* On-disk changed catalogs should have overridden in-memory not-changed ones. */
806 const AssetCatalog *ruzena_face = loaded_service.find_catalog(UUID_POSES_RUZENA_FACE);
807 EXPECT_EQ("character/Ružena/poselib/gezicht", ruzena_face->path.str());
808}
809
810TEST_F(AssetCatalogTest, refresh_catalogs_with_modification)
811{
812 const CatalogFilePath cdf_dir = create_temp_path();
813 const CatalogFilePath original_cdf_file = asset_library_root_ + SEP_STR +
814 "blender_assets.cats.txt";
815 const CatalogFilePath modified_cdf_file = asset_library_root_ + SEP_STR +
816 "catalog_reload_test.cats.txt";
817 const CatalogFilePath temp_cdf_file = cdf_dir + "blender_assets.cats.txt";
818 ASSERT_EQ(0, BLI_copy(original_cdf_file.c_str(), temp_cdf_file.c_str()));
819
820 /* Load the unmodified, original CDF. */
821 TestableAssetCatalogService service(asset_library_root_);
822 service.load_from_disk(cdf_dir);
823
824 /* === Perform changes that should be handled gracefully by the reloading code: */
825
826 /* 1. Delete a subtree of catalogs. */
828 /* 2. Rename a catalog. */
830 service.update_catalog_path(UUID_POSES_ELLIE_TRAILING_SLASH, "character/Ellie/test-value");
831
832 /* Copy a modified file, to mimic a situation where someone changed the
833 * CDF after we loaded it. */
834 ASSERT_EQ(0, BLI_copy(modified_cdf_file.c_str(), temp_cdf_file.c_str()));
835
836 AssetCatalog *const ellie_whitespace_before_reload = service.find_catalog(
838
839 /* This should merge the on-disk file with our catalogs. */
840 service.reload_catalogs();
841
842 /* === Test that the expected catalogs are there. */
843 EXPECT_NE(nullptr, service.find_catalog(UUID_POSES_ELLIE));
844 EXPECT_NE(nullptr, service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
845 EXPECT_NE(nullptr, service.find_catalog(UUID_POSES_ELLIE_TRAILING_SLASH));
846
847 /* === Test changes made to the CDF: */
848
849 /* Removed from the file. */
851 /* Added to the file. */
852 EXPECT_NE(nullptr, service.find_catalog(UUID_AGENT_47));
853 /* Path modified in file. */
854 AssetCatalog *ellie_whitespace_after_reload = service.find_catalog(UUID_POSES_ELLIE_WHITESPACE);
855 EXPECT_EQ(AssetCatalogPath("whitespace from file"), ellie_whitespace_after_reload->path);
856 EXPECT_NE(ellie_whitespace_after_reload, ellie_whitespace_before_reload);
857 /* Simple name modified in file. */
858 EXPECT_EQ(std::string("Hah simple name after all"),
860
861 /* === Test persistence of in-memory changes: */
862
863 /* This part of the tree we deleted, but still existed in the CDF. They should remain deleted
864 * after reloading: */
865 EXPECT_EQ(nullptr, service.find_catalog(UUID_POSES_RUZENA));
868
869 /* This catalog had its path changed in the test and in the CDF. The change from the test (i.e.
870 * the in-memory, yet-unsaved change) should persist. */
871 EXPECT_EQ(AssetCatalogPath("character/Ellie/test-value"),
873
874 /* Overwrite the modified file. This should merge the on-disk file with our catalogs, and clear
875 * the "has_unsaved_changes" flags. */
876 service.write_to_disk(cdf_dir + "phony.blend");
877
879 << "The catalogs whose path we changed should now be saved";
880 EXPECT_TRUE(service.get_deleted_catalogs().is_empty())
881 << "Deleted catalogs should not be remembered after saving.";
882}
883
885{
886 const CatalogFilePath cdf_dir = create_temp_path();
887 const CatalogFilePath original_cdf_file = asset_library_root_ + SEP_STR +
888 "blender_assets.cats.txt";
889 const CatalogFilePath writable_cdf_file = cdf_dir + SEP_STR + "blender_assets.cats.txt";
890 ASSERT_EQ(0, BLI_copy(original_cdf_file.c_str(), writable_cdf_file.c_str()));
891
892 /* Read a CDF, modify, and write it. */
893 TestableAssetCatalogService service(cdf_dir);
894 service.load_from_disk();
896 service.tag_has_unsaved_changes();
897 service.write_to_disk(cdf_dir + "phony.blend");
898
899 const CatalogFilePath backup_path = writable_cdf_file + "~";
900 ASSERT_TRUE(BLI_is_file(backup_path.c_str()));
901
902 AssetCatalogService loaded_service;
903 loaded_service.load_from_disk(backup_path);
904
905 /* Test that the expected catalogs are there, including the deleted one.
906 * This is the backup, after all. */
907 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE));
908 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_WHITESPACE));
909 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_ELLIE_TRAILING_SLASH));
910 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA));
911 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_HAND));
912 EXPECT_NE(nullptr, loaded_service.find_catalog(UUID_POSES_RUZENA_FACE));
913}
914
916{
917 const bUUID cat2_uuid("22222222-b847-44d9-bdca-ff04db1c24f5");
918 const bUUID cat4_uuid("11111111-b847-44d9-bdca-ff04db1c24f5"); /* Sorts earlier than above. */
919 const AssetCatalog cat1(BLI_uuid_generate_random(), "simple/path/child", "");
920 const AssetCatalog cat2(cat2_uuid, "simple/path", "");
921 const AssetCatalog cat3(BLI_uuid_generate_random(), "complex/path/...or/is/it?", "");
922 const AssetCatalog cat4(
923 cat4_uuid, "simple/path", "different ID, same path"); /* should be kept */
924 const AssetCatalog cat5(cat4_uuid, "simple/path", "same ID, same path"); /* disappears */
925
927 by_path.insert(&cat1);
928 by_path.insert(&cat2);
929 by_path.insert(&cat3);
930 by_path.insert(&cat4);
931 by_path.insert(&cat5);
932
933 AssetCatalogOrderedSet::const_iterator set_iter = by_path.begin();
934
935 EXPECT_EQ(1, by_path.count(&cat1));
936 EXPECT_EQ(1, by_path.count(&cat2));
937 EXPECT_EQ(1, by_path.count(&cat3));
938 EXPECT_EQ(1, by_path.count(&cat4));
939 ASSERT_EQ(4, by_path.size()) << "Expecting cat5 to not be stored in the set, as it duplicates "
940 "an already-existing path + UUID";
941
942 EXPECT_EQ(cat3.catalog_id, (*(set_iter++))->catalog_id); /* complex/path */
943 EXPECT_EQ(cat4.catalog_id, (*(set_iter++))->catalog_id); /* simple/path with 111.. ID */
944 EXPECT_EQ(cat2.catalog_id, (*(set_iter++))->catalog_id); /* simple/path with 222.. ID */
945 EXPECT_EQ(cat1.catalog_id, (*(set_iter++))->catalog_id); /* simple/path/child */
946
947 if (set_iter != by_path.end()) {
948 const AssetCatalog *next_cat = *set_iter;
949 FAIL() << "Did not expect more items in the set, had at least " << next_cat->catalog_id << ":"
950 << next_cat->path;
951 }
952}
953
954TEST_F(AssetCatalogTest, order_by_path_and_first_seen)
955{
956 AssetCatalogService service;
957 service.load_from_disk(asset_library_root_);
958
959 const bUUID first_seen_uuid("3d451c87-27d1-40fd-87fc-f4c9e829c848");
960 const bUUID first_sorted_uuid("00000000-0000-0000-0000-000000000001");
961 const bUUID last_sorted_uuid("ffffffff-ffff-ffff-ffff-ffffffffffff");
962
963 AssetCatalog first_seen_cat(first_seen_uuid, "simple/path/child", "");
964 const AssetCatalog first_sorted_cat(first_sorted_uuid, "simple/path/child", "");
965 const AssetCatalog last_sorted_cat(last_sorted_uuid, "simple/path/child", "");
966
967 /* Mimic that this catalog was first-seen when loading from disk. */
968 first_seen_cat.flags.is_first_loaded = true;
969
970 /* Just an assertion of the defaults; this is more to avoid confusing errors later on than an
971 * actual test of these defaults. */
972 ASSERT_FALSE(first_sorted_cat.flags.is_first_loaded);
973 ASSERT_FALSE(last_sorted_cat.flags.is_first_loaded);
974
976 by_path.insert(&first_seen_cat);
977 by_path.insert(&first_sorted_cat);
978 by_path.insert(&last_sorted_cat);
979
980 AssetCatalogOrderedSet::const_iterator set_iter = by_path.begin();
981
982 EXPECT_EQ(1, by_path.count(&first_seen_cat));
983 EXPECT_EQ(1, by_path.count(&first_sorted_cat));
984 EXPECT_EQ(1, by_path.count(&last_sorted_cat));
985 ASSERT_EQ(3, by_path.size());
986
987 EXPECT_EQ(first_seen_uuid, (*(set_iter++))->catalog_id);
988 EXPECT_EQ(first_sorted_uuid, (*(set_iter++))->catalog_id);
989 EXPECT_EQ(last_sorted_uuid, (*(set_iter++))->catalog_id);
990
991 if (set_iter != by_path.end()) {
992 const AssetCatalog *next_cat = *set_iter;
993 FAIL() << "Did not expect more items in the set, had at least " << next_cat->catalog_id << ":"
994 << next_cat->path;
995 }
996}
997
998TEST_F(AssetCatalogTest, create_missing_catalogs)
999{
1000 TestableAssetCatalogService new_service;
1001 new_service.create_catalog("path/with/missing/parents");
1002
1003 EXPECT_EQ(nullptr, new_service.find_catalog_by_path("path/with/missing"))
1004 << "Missing parents should not be immediately created.";
1005 EXPECT_EQ(nullptr, new_service.find_catalog_by_path("")) << "Empty path should never be valid";
1006
1007 new_service.create_missing_catalogs();
1008
1009 EXPECT_NE(nullptr, new_service.find_catalog_by_path("path/with/missing"));
1010 EXPECT_NE(nullptr, new_service.find_catalog_by_path("path/with"));
1011 EXPECT_NE(nullptr, new_service.find_catalog_by_path("path"));
1012 EXPECT_EQ(nullptr, new_service.find_catalog_by_path(""))
1013 << "Empty path should never be valid, even when after missing catalogs";
1014}
1015
1016TEST_F(AssetCatalogTest, create_missing_catalogs_after_loading)
1017{
1018 TestableAssetCatalogService loaded_service(asset_library_root_);
1019 loaded_service.load_from_disk();
1020
1021 const AssetCatalog *cat_char = loaded_service.find_catalog_by_path("character");
1022 const AssetCatalog *cat_ellie = loaded_service.find_catalog_by_path("character/Ellie");
1023 const AssetCatalog *cat_ruzena = loaded_service.find_catalog_by_path("character/Ružena");
1024 ASSERT_NE(nullptr, cat_char) << "Missing parents should be created immediately after loading.";
1025 ASSERT_NE(nullptr, cat_ellie) << "Missing parents should be created immediately after loading.";
1026 ASSERT_NE(nullptr, cat_ruzena) << "Missing parents should be created immediately after loading.";
1027
1028 EXPECT_TRUE(cat_char->flags.has_unsaved_changes)
1029 << "Missing parents should be marked as having changes.";
1030 EXPECT_TRUE(cat_ellie->flags.has_unsaved_changes)
1031 << "Missing parents should be marked as having changes.";
1032 EXPECT_TRUE(cat_ruzena->flags.has_unsaved_changes)
1033 << "Missing parents should be marked as having changes.";
1034
1035 const AssetCatalogDefinitionFile *cdf = loaded_service.get_catalog_definition_file();
1036 ASSERT_NE(nullptr, cdf);
1037 EXPECT_TRUE(cdf->contains(cat_char->catalog_id)) << "Missing parents should be saved to a CDF.";
1038 EXPECT_TRUE(cdf->contains(cat_ellie->catalog_id)) << "Missing parents should be saved to a CDF.";
1039 EXPECT_TRUE(cdf->contains(cat_ruzena->catalog_id))
1040 << "Missing parents should be saved to a CDF.";
1041
1042 /* Check that each missing parent is only created once. The CDF contains multiple paths that
1043 * could trigger the creation of missing parents, so this test makes sense. */
1044 EXPECT_EQ(1, loaded_service.count_catalogs_with_path("character"));
1045 EXPECT_EQ(1, loaded_service.count_catalogs_with_path("character/Ellie"));
1046 EXPECT_EQ(1, loaded_service.count_catalogs_with_path("character/Ružena"));
1047}
1048
1049TEST_F(AssetCatalogTest, create_catalog_filter)
1050{
1051 AssetCatalogService service(asset_library_root_);
1052 service.load_from_disk();
1053
1054 /* Alias for the same catalog as the main one. */
1055 AssetCatalog *alias_ruzena = service.create_catalog("character/Ružena/poselib");
1056 /* Alias for a sub-catalog. */
1057 AssetCatalog *alias_ruzena_hand = service.create_catalog("character/Ružena/poselib/hand");
1058
1060
1061 /* Positive test for loaded-from-disk catalogs. */
1062 EXPECT_TRUE(filter.contains(UUID_POSES_RUZENA))
1063 << "Main catalog should be included in the filter.";
1064 EXPECT_TRUE(filter.contains(UUID_POSES_RUZENA_HAND))
1065 << "Sub-catalog should be included in the filter.";
1066 EXPECT_TRUE(filter.contains(UUID_POSES_RUZENA_FACE))
1067 << "Sub-catalog should be included in the filter.";
1068
1069 /* Positive test for newly-created catalogs. */
1070 EXPECT_TRUE(filter.contains(alias_ruzena->catalog_id))
1071 << "Alias of main catalog should be included in the filter.";
1072 EXPECT_TRUE(filter.contains(alias_ruzena_hand->catalog_id))
1073 << "Alias of sub-catalog should be included in the filter.";
1074
1075 /* Negative test for unrelated catalogs. */
1076 EXPECT_FALSE(filter.contains(BLI_uuid_nil())) << "Nil catalog should not be included.";
1077 EXPECT_FALSE(filter.contains(UUID_ID_WITHOUT_PATH));
1078 EXPECT_FALSE(filter.contains(UUID_POSES_ELLIE));
1079 EXPECT_FALSE(filter.contains(UUID_POSES_ELLIE_WHITESPACE));
1080 EXPECT_FALSE(filter.contains(UUID_POSES_ELLIE_TRAILING_SLASH));
1081 EXPECT_FALSE(filter.contains(UUID_WITHOUT_SIMPLENAME));
1082}
1083
1084TEST_F(AssetCatalogTest, create_catalog_filter_for_unknown_uuid)
1085{
1086 AssetCatalogService service;
1087 const bUUID unknown_uuid = BLI_uuid_generate_random();
1088
1089 AssetCatalogFilter filter = service.create_catalog_filter(unknown_uuid);
1090 EXPECT_TRUE(filter.contains(unknown_uuid));
1091
1092 EXPECT_FALSE(filter.contains(BLI_uuid_nil())) << "Nil catalog should not be included.";
1093 EXPECT_FALSE(filter.contains(UUID_POSES_ELLIE));
1094}
1095
1096TEST_F(AssetCatalogTest, create_catalog_filter_for_unassigned_assets)
1097{
1098 AssetCatalogService service;
1099
1101 EXPECT_TRUE(filter.contains(BLI_uuid_nil()));
1102 EXPECT_FALSE(filter.contains(UUID_POSES_ELLIE));
1103}
1104
1105TEST_F(AssetCatalogTest, cat_collection_deep_copy__empty)
1106{
1107 const AssetCatalogCollection empty;
1108 auto copy = empty.deep_copy();
1109 EXPECT_NE(&empty, copy.get());
1110}
1111
1113 public:
1115 {
1116 return catalogs_;
1117 }
1127 {
1128 catalog_definition_file_ = std::make_unique<AssetCatalogDefinitionFile>(file_path);
1130 }
1131};
1132
1133TEST_F(AssetCatalogTest, cat_collection_deep_copy__nonempty_nocdf)
1134{
1136 auto cat1 = std::make_unique<AssetCatalog>(UUID_POSES_RUZENA, "poses/Henrik", "");
1137 auto cat2 = std::make_unique<AssetCatalog>(UUID_POSES_RUZENA_FACE, "poses/Henrik/face", "");
1138 auto cat3 = std::make_unique<AssetCatalog>(UUID_POSES_RUZENA_HAND, "poses/Henrik/hands", "");
1139 cat3->flags.is_deleted = true;
1140
1141 AssetCatalog *cat1_ptr = cat1.get();
1142 AssetCatalog *cat3_ptr = cat3.get();
1143
1144 catcoll.get_catalogs().add_new(cat1->catalog_id, std::move(cat1));
1145 catcoll.get_catalogs().add_new(cat2->catalog_id, std::move(cat2));
1146 catcoll.get_deleted_catalogs().add_new(cat3->catalog_id, std::move(cat3));
1147
1148 auto copy = catcoll.deep_copy();
1149 EXPECT_NE(&catcoll, copy.get());
1150
1151 TestableAssetCatalogCollection *testcopy = reinterpret_cast<TestableAssetCatalogCollection *>(
1152 copy.get());
1153
1154 /* Test catalogs & deleted catalogs. */
1155 EXPECT_EQ(2, testcopy->get_catalogs().size());
1156 EXPECT_EQ(1, testcopy->get_deleted_catalogs().size());
1157
1158 ASSERT_TRUE(testcopy->get_catalogs().contains(UUID_POSES_RUZENA));
1159 ASSERT_TRUE(testcopy->get_catalogs().contains(UUID_POSES_RUZENA_FACE));
1160 ASSERT_TRUE(testcopy->get_deleted_catalogs().contains(UUID_POSES_RUZENA_HAND));
1161
1162 EXPECT_NE(nullptr, testcopy->get_catalogs().lookup(UUID_POSES_RUZENA));
1163 EXPECT_NE(cat1_ptr, testcopy->get_catalogs().lookup(UUID_POSES_RUZENA).get())
1164 << "AssetCatalogs should be actual copies.";
1165
1166 EXPECT_NE(nullptr, testcopy->get_deleted_catalogs().lookup(UUID_POSES_RUZENA_HAND));
1167 EXPECT_NE(cat3_ptr, testcopy->get_deleted_catalogs().lookup(UUID_POSES_RUZENA_HAND).get())
1168 << "AssetCatalogs should be actual copies.";
1169}
1170
1178
1179TEST_F(AssetCatalogTest, cat_collection_deep_copy__nonempty_cdf)
1180{
1182 auto cat1 = std::make_unique<AssetCatalog>(UUID_POSES_RUZENA, "poses/Henrik", "");
1183 auto cat2 = std::make_unique<AssetCatalog>(UUID_POSES_RUZENA_FACE, "poses/Henrik/face", "");
1184 auto cat3 = std::make_unique<AssetCatalog>(UUID_POSES_RUZENA_HAND, "poses/Henrik/hands", "");
1185 cat3->flags.is_deleted = true;
1186
1187 AssetCatalog *cat1_ptr = cat1.get();
1188 AssetCatalog *cat2_ptr = cat2.get();
1189 AssetCatalog *cat3_ptr = cat3.get();
1190
1191 catcoll.get_catalogs().add_new(cat1->catalog_id, std::move(cat1));
1192 catcoll.get_catalogs().add_new(cat2->catalog_id, std::move(cat2));
1193 catcoll.get_deleted_catalogs().add_new(cat3->catalog_id, std::move(cat3));
1194
1196 "path/to/somewhere.cats.txt");
1197 cdf->add_new(cat1_ptr);
1198 cdf->add_new(cat2_ptr);
1199 cdf->add_new(cat3_ptr);
1200
1201 /* Test CDF remapping. */
1202 auto copy = catcoll.deep_copy();
1203 TestableAssetCatalogCollection *testable_copy = static_cast<TestableAssetCatalogCollection *>(
1204 copy.get());
1205
1207 testable_copy->get_catalog_definition_file());
1208 EXPECT_EQ(testable_copy->get_catalogs().lookup(UUID_POSES_RUZENA).get(),
1209 cdf_copy->get_catalogs().lookup(UUID_POSES_RUZENA))
1210 << "AssetCatalog pointers should have been remapped to the copy.";
1211
1213 cdf_copy->get_catalogs().lookup(UUID_POSES_RUZENA_HAND))
1214 << "Deleted AssetCatalog pointers should have been remapped to the copy.";
1215}
1216
1217TEST_F(AssetCatalogTest, undo_redo_one_step)
1218{
1219 TestableAssetCatalogService service(asset_library_root_);
1220 service.load_from_disk();
1221
1222 EXPECT_FALSE(service.is_undo_possbile());
1223 EXPECT_FALSE(service.is_redo_possbile());
1224
1225 service.create_catalog("some/catalog/path");
1226 EXPECT_FALSE(service.is_undo_possbile())
1227 << "Undo steps should be created explicitly, and not after creating any catalog.";
1228
1229 service.undo_push();
1230 const bUUID other_catalog_id = service.create_catalog("other/catalog/path")->catalog_id;
1231 EXPECT_TRUE(service.is_undo_possbile())
1232 << "Undo should be possible after creating an undo snapshot.";
1233
1234 /* Undo the creation of the catalog. */
1235 service.undo();
1236 EXPECT_FALSE(service.is_undo_possbile())
1237 << "Undoing the only stored step should make it impossible to undo further.";
1238 EXPECT_TRUE(service.is_redo_possbile()) << "Undoing a step should make redo possible.";
1239 EXPECT_EQ(nullptr, service.find_catalog_by_path("other/catalog/path"))
1240 << "Undone catalog should not exist after undo.";
1241 EXPECT_NE(nullptr, service.find_catalog_by_path("some/catalog/path"))
1242 << "First catalog should still exist after undo.";
1243 EXPECT_FALSE(service.get_catalog_definition_file()->contains(other_catalog_id))
1244 << "The CDF should also not contain the undone catalog.";
1245
1246 /* Redo the creation of the catalog. */
1247 service.redo();
1248 EXPECT_TRUE(service.is_undo_possbile())
1249 << "Undoing and then redoing a step should make it possible to undo again.";
1250 EXPECT_FALSE(service.is_redo_possbile())
1251 << "Undoing and then redoing a step should make redo impossible.";
1252 EXPECT_NE(nullptr, service.find_catalog_by_path("other/catalog/path"))
1253 << "Redone catalog should exist after redo.";
1254 EXPECT_NE(nullptr, service.find_catalog_by_path("some/catalog/path"))
1255 << "First catalog should still exist after redo.";
1256 EXPECT_TRUE(service.get_catalog_definition_file()->contains(other_catalog_id))
1257 << "The CDF should contain the redone catalog.";
1258}
1259
1260TEST_F(AssetCatalogTest, undo_redo_more_complex)
1261{
1262 TestableAssetCatalogService service(asset_library_root_);
1263 service.load_from_disk();
1264
1265 service.undo_push();
1266 service.find_catalog(UUID_POSES_ELLIE_WHITESPACE)->simple_name = "Edited simple name";
1267
1268 service.undo_push();
1269 service.find_catalog(UUID_POSES_ELLIE)->path = "poselib/EllieWithEditedPath";
1270
1271 service.undo();
1272 service.undo();
1273
1274 service.undo_push();
1275 service.find_catalog(UUID_POSES_ELLIE)->simple_name = "Ellie Simple";
1276
1277 EXPECT_FALSE(service.is_redo_possbile())
1278 << "After storing an undo snapshot, the redo buffer should be empty.";
1279 EXPECT_TRUE(service.is_undo_possbile())
1280 << "After storing an undo snapshot, undoing should be possible";
1281
1282 EXPECT_EQ(service.find_catalog(UUID_POSES_ELLIE)->simple_name, "Ellie Simple"); /* Not undone. */
1284 "POSES_ELLIE WHITESPACE"); /* Undone. */
1285 EXPECT_EQ(service.find_catalog(UUID_POSES_ELLIE)->path, "character/Ellie/poselib"); /* Undone. */
1286}
1287
1288} // namespace blender::asset_system::tests
struct bUserAssetLibrary * BKE_preferences_asset_library_add(struct UserDef *userdef, const char *name, const char *dirpath) ATTR_NONNULL(1)
void BKE_preferences_asset_library_remove(struct UserDef *userdef, struct bUserAssetLibrary *library) ATTR_NONNULL()
EXPECT_EQ(BLI_expr_pylike_eval(expr, nullptr, 0, &result), EXPR_PYLIKE_INVALID)
File and directory operations.
bool BLI_file_touch(const char *filepath) ATTR_NONNULL(1)
Definition fileops_c.cc:316
int BLI_exists(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
Definition storage.cc:360
int BLI_copy(const char *path_src, const char *path_dst) ATTR_NONNULL()
bool BLI_dir_create_recursive(const char *dirname) ATTR_NONNULL()
Definition fileops_c.cc:414
bool BLI_is_file(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
Definition storage.cc:448
size_t BLI_file_size(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
Definition storage.cc:226
bool BLI_is_dir(const char *path) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL()
Definition storage.cc:443
void BLI_path_slash_native(char *path) ATTR_NONNULL(1)
bUUID BLI_uuid_nil()
Definition uuid.cc:73
bool BLI_uuid_is_nil(bUUID uuid)
Definition uuid.cc:79
bUUID BLI_uuid_generate_random()
Definition uuid.cc:24
#define U
long long int int64_t
const Value & lookup(const Key &key) const
Definition BLI_map.hh:545
void add_new(const Key &key, const Value &value)
Definition BLI_map.hh:265
int64_t size() const
Definition BLI_map.hh:976
bool is_empty() const
Definition BLI_map.hh:986
bool contains(const Key &key) const
Definition BLI_map.hh:353
std::unique_ptr< AssetCatalogDefinitionFile > catalog_definition_file_
std::unique_ptr< AssetCatalogCollection > deep_copy() const
AssetCatalogFilter create_catalog_filter(CatalogID active_catalog_id) const
AssetCatalog * find_catalog_by_path(const AssetCatalogPath &path) const
AssetCatalog * find_catalog(CatalogID catalog_id) const
bool write_to_disk(const CatalogFilePath &blend_file_path)
AssetCatalogService(const CatalogFilePath &asset_library_root={})
AssetCatalog * create_catalog(const AssetCatalogPath &catalog_path)
void update_catalog_path(CatalogID catalog_id, const AssetCatalogPath &new_catalog_path)
void tag_has_unsaved_changes(AssetCatalog *edited_catalog=nullptr)
static const CatalogFilePath DEFAULT_CATALOG_FILENAME
const AssetCatalogDefinitionFile * get_catalog_definition_file() const
struct blender::asset_system::AssetCatalog::Flags flags
void save_from_memory_into_existing_asset_lib(const bool should_top_level_cdf_exist)
static void expect_tree_items(const AssetCatalogTree &tree, const std::vector< AssetCatalogPath > &expected_paths)
AssetCatalogDefinitionFile * allocate_catalog_definition_file(StringRef file_path)
const OwningAssetCatalogMap & get_deleted_catalogs() const
TestableAssetCatalogService(const CatalogFilePath &asset_library_root)
const AssetCatalogDefinitionFile * get_catalog_definition_file()
int64_t count_catalogs_with_path(const CatalogFilePath &path)
KDTree_3d * tree
#define filter
int count
const bUUID UUID_POSES_RUZENA_HAND("81811c31-1a88-4bd7-bb34-c6fc2607a12e")
TEST_F(AssetCatalogTest, load_single_file)
const bUUID UUID_ANOTHER_RUZENA("00000000-d9fa-4b91-b704-e6af1f1339ef")
const bUUID UUID_POSES_RUZENA_FACE("82162c1f-06cc-4d91-a9bf-4f72c104e348")
const bUUID UUID_POSES_ELLIE_TRAILING_SLASH("3376b94b-a28d-4d05-86c1-bf30b937130d")
const bUUID UUID_POSES_ELLIE_BACKSLASHES("a51e17ae-34fc-47d5-ba0f-64c2c9b771f7")
const bUUID UUID_POSES_ELLIE("df60e1f6-2259-475b-93d9-69a1b4a8db78")
const bUUID UUID_WITHOUT_SIMPLENAME("d7916a31-6ca9-4909-955f-182ca2b81fa3")
const bUUID UUID_POSES_RUZENA("79a4f887-ab60-4bd4-94da-d572e27d6aed")
const bUUID UUID_POSES_ELLIE_WHITESPACE("b06132f6-5687-4751-a6dd-392740eb3c46")
const bUUID UUID_ID_WITHOUT_PATH("e34dd2c5-5d2e-4668-9794-1db5de2a4f71")
const bUUID UUID_AGENT_47("c5744ba5-43f5-4f73-8e52-010ad4a61b34")
Map< CatalogID, std::unique_ptr< AssetCatalog > > OwningAssetCatalogMap
std::set< const AssetCatalog *, AssetCatalogLessThan > AssetCatalogOrderedSet
static void copy(bNodeTree *dest_ntree, bNode *dest_node, const bNode *src_node)
char catalog_simple_name[64]
#define SEP_STR
Definition unit.cc:39