BKE main: Add 'merge' utils to mergeone Main content into another. #115671

Merged
Bastien Montagne merged 2 commits from mont29/blender:tmp-main-merge into main 2023-12-02 16:25:09 +01:00
6 changed files with 384 additions and 28 deletions

View File

@ -72,6 +72,13 @@ enum {
* the 'separate' mesh operator.
*/
ID_REMAP_FORCE_OBDATA_IN_EDITMODE = 1 << 7,
/** Do remapping of `lib` Library pointers of IDs (by default these are completely ignored).
*
* WARNING: Use with caution. This is currently a 'raw' remapping, with no further processing. In
* particular, DO NOT use this to make IDs local (i.e. remap a library pointer to NULL), unless
* the calling code takes care of the rest of the required changes (ID tags & flags updates,
* etc.). */
ID_REMAP_DO_LIBRARY_POINTERS = 1 << 8,
/**
* Don't touch the special user counts (use when the 'old' remapped ID remains in use):

View File

@ -33,6 +33,7 @@ struct IDNameLib_Map;
struct ImBuf;
struct Library;
struct MainLock;
struct ReportList;
struct UniqueName_Map;
/**
@ -258,6 +259,41 @@ struct Main {
Main *BKE_main_new(void);
void BKE_main_free(Main *mainvar);
/** Struct packaging log/report info about a Main merge result. */
struct MainMergeReport {
ReportList *reports = nullptr;
/** Number of IDs from source Main that have been moved into destination Main. */
int num_merged_ids = 0;
/** Number of (non-library) IDs from source Main that were expected to have a matching ID in
* destination Main, but did not. These have not been moved, and their usages have been remapped
* to null. */
int num_unknown_ids = 0;
/** Number of (non-library) IDs from source Main that already had a matching ID in destination
* Main. */
int num_remapped_ids = 0;
/** Number of Library IDs from source Main that already had a matching Library ID in destination
* Main. */
int num_remapped_libraries = 0;
};
/** Merge the content of `bmain_src` into `bmain_dst`.
*
* In case of collision (ID from same library with same name), the existing ID in `bmain_dst` is
* kept, the one from `bmain_src` is left in its original Main, and its usages in `bmain_dst` (from
* newly moved-in IDs) are remapped to its matching counterpart already in `bmain_dst`.
*
* Libraries are also de-duplicated, based on their absolute filepath, and remapped accordingly.
* Note that local IDs in source Main always remain local IDs in destination Main.
*
* In case some source IDs are linked data from the blendfile of `bmain_dst`, they are never moved.
* If a matching destination local ID is found, their usage get remapped as expected, otherwise
* they are dropped, their usages are remapped to null, and a warning is printed.
*
* Since `bmain_src` is either empty or contains left-over IDs with (likely) invalid ID
* relationships and other potential issues after the merge, it is always freed. */
void BKE_main_merge(Main *bmain_dst, Main **r_bmain_src, MainMergeReport &reports);
/**
* Check whether given `bmain` is empty or contains some IDs.
*/

View File

@ -847,6 +847,7 @@ if(WITH_GTESTS)
intern/lib_id_remapper_test.cc
intern/lib_id_test.cc
intern/lib_remap_test.cc
intern/main_test.cc
intern/nla_test.cc
intern/tracking_test.cc
)

View File

@ -487,13 +487,15 @@ static void libblock_remap_data(
{
IDRemap id_remap_data = {eIDRemapType(0)};
const bool include_ui = (remap_flags & ID_REMAP_FORCE_UI_POINTERS) != 0;
const int foreach_id_flags = (((remap_flags & ID_REMAP_FORCE_INTERNAL_RUNTIME_POINTERS) != 0 ?
IDWALK_DO_INTERNAL_RUNTIME_POINTERS :
IDWALK_NOP) |
(include_ui ? IDWALK_INCLUDE_UI : IDWALK_NOP) |
((remap_flags & ID_REMAP_NO_ORIG_POINTERS_ACCESS) != 0 ?
IDWALK_NO_ORIG_POINTERS_ACCESS :
IDWALK_NOP));
const int foreach_id_flags =
(((remap_flags & ID_REMAP_FORCE_INTERNAL_RUNTIME_POINTERS) != 0 ?
IDWALK_DO_INTERNAL_RUNTIME_POINTERS :
IDWALK_NOP) |
(include_ui ? IDWALK_INCLUDE_UI : IDWALK_NOP) |
((remap_flags & ID_REMAP_NO_ORIG_POINTERS_ACCESS) != 0 ? IDWALK_NO_ORIG_POINTERS_ACCESS :
IDWALK_NOP) |
((remap_flags & ID_REMAP_DO_LIBRARY_POINTERS) != 0 ? IDWALK_DO_LIBRARY_POINTER : IDWALK_NOP));
id_remap_data.id_remapper = id_remapper;
id_remap_data.type = remap_type;

View File

@ -9,27 +9,37 @@
*/
#include <cstring>
#include <iostream>
#include "CLG_log.h"
#include "MEM_guardedalloc.h"
#include "BLI_blenlib.h"
#include "BLI_ghash.h"
#include "BLI_map.hh"
#include "BLI_mempool.h"
#include "BLI_threads.h"
#include "BLI_vector.hh"
#include "DNA_ID.h"
#include "BKE_bpath.h"
#include "BKE_global.h"
#include "BKE_idtype.h"
#include "BKE_lib_id.h"
#include "BKE_lib_query.h"
#include "BKE_lib_remap.hh"
#include "BKE_main.hh"
#include "BKE_main_idmap.hh"
#include "BKE_main_namemap.hh"
#include "BKE_report.h"
#include "IMB_imbuf.h"
#include "IMB_imbuf_types.h"
static CLG_LogRef LOG = {"bke.main"};
Main *BKE_main_new()
{
Main *bmain = static_cast<Main *>(MEM_callocN(sizeof(Main), "new main"));
@ -156,6 +166,271 @@ void BKE_main_free(Main *mainvar)
MEM_freeN(mainvar);
}
static bool are_ids_from_different_mains_matching(Main *bmain_1, ID *id_1, Main *bmain_2, ID *id_2)
{
/* Both IDs should not be null at the same time.
*
* NOTE: E.g. `id_1` may be null, in case `id_2` is a Library ID which path is the filepath of
* `bmain_1`. */
BLI_assert(id_1 || id_2);
/* Special handling for libraries, since their filepaths is used then, not their ID names.
*
* NOTE: In library case, this call should always return true, since given data should always
* match. The asserts below merely ensure that expected conditions are always met:
* - A given library absolute filepath should never match its own bmain filepath.
* - If both given libraries are non-null:
* - Their absolute filepath should match.
* - Neither of their absolute filepaths should match any of the bmain filepaths.
* - If one of the library is null:
* - The other library should match the bmain filepath of the null library. */
if ((!id_1 && GS(id_2->name) == ID_LI) || GS(id_1->name) == ID_LI) {
BLI_assert(!id_1 || !ID_IS_LINKED(id_1));
BLI_assert(!id_2 || !ID_IS_LINKED(id_2));
Library *lib_1 = reinterpret_cast<Library *>(id_1);
Library *lib_2 = reinterpret_cast<Library *>(id_2);
if (lib_1 && lib_2) {
BLI_assert(STREQ(lib_1->filepath_abs, lib_2->filepath_abs));
}
if (lib_1) {
BLI_assert(!STREQ(lib_1->filepath_abs, bmain_1->filepath));
if (lib_2) {
BLI_assert(!STREQ(lib_1->filepath_abs, bmain_2->filepath));
}
else {
BLI_assert(STREQ(lib_1->filepath_abs, bmain_2->filepath));
}
}
if (lib_2) {
BLI_assert(!STREQ(lib_2->filepath_abs, bmain_2->filepath));
if (lib_1) {
BLI_assert(!STREQ(lib_2->filepath_abs, bmain_1->filepath));
}
else {
BLI_assert(STREQ(lib_2->filepath_abs, bmain_1->filepath));
}
}
return true;
}
/* Now both IDs are expected to be valid data, and caller is expected to have ensured already
* that they have the same name. */
BLI_assert(id_1 && id_2);
BLI_assert(STREQ(id_1->name, id_2->name));
if (!id_1->lib && !id_2->lib) {
return true;
}
if (id_1->lib && id_2->lib) {
if (id_1->lib == id_2->lib) {
return true;
}
if (STREQ(id_1->lib->filepath_abs, id_2->lib->filepath_abs)) {
return true;
}
return false;
}
/* In case one Main is the library of the ID from the other Main. */
if (id_1->lib) {
if (STREQ(id_1->lib->filepath_abs, bmain_2->filepath)) {
return true;
}
return false;
}
if (id_2->lib) {
if (STREQ(id_2->lib->filepath_abs, bmain_1->filepath)) {
return true;
}
return false;
}
BLI_assert_unreachable();
return false;
}
static void main_merge_add_id_to_move(Main *bmain_dst,
blender::Map<std::string, blender::Vector<ID *>> &id_map_dst,
ID *id_src,
IDRemapper *id_remapper,
blender::Vector<ID *> &ids_to_move,
const bool is_library,
MainMergeReport &reports)
{
const bool is_id_src_linked(id_src->lib);
bool is_id_src_from_bmain_dst = false;
if (is_id_src_linked) {
BLI_assert(!is_library);
blender::Vector<ID *> id_src_lib_dst = id_map_dst.lookup_default(id_src->lib->filepath_abs,
{});
/* The current library of the source ID would be remapped to null, which means that it comes
* from the destination Main. */
is_id_src_from_bmain_dst = !id_src_lib_dst.is_empty() && !id_src_lib_dst[0];
}
std::cout << id_src->name << " is linked from dst Main: " << is_id_src_from_bmain_dst << "\n";
std::cout.flush();
if (is_id_src_from_bmain_dst) {
/* Do not move an ID supposed to be from `bmain_dst` (used as library in `bmain_src`) into
* `bmain_src`. Fact that no match was found is worth a warning, although it could happen
* e.g. in case `bmain_dst` has been updated since it file was loaded as library in
* `bmain_src`. */
CLOG_WARN(&LOG,
"ID '%s' defined in source Main as linked from destination Main (file '%s') not "
"found in given destination Main",
id_src->name,
bmain_dst->filepath);
BKE_id_remapper_add(id_remapper, id_src, nullptr);
reports.num_unknown_ids++;
}
else {
ids_to_move.append(id_src);
}
}
void BKE_main_merge(Main *bmain_dst, Main **r_bmain_src, MainMergeReport &reports)
{
Main *bmain_src = *r_bmain_src;
/* NOTE: Dedicated mapping type is needed here, to handle propoerly the library cases. */
blender::Map<std::string, blender::Vector<ID *>> id_map_dst;
ID *id_iter_dst, *id_iter_src;
FOREACH_MAIN_ID_BEGIN (bmain_dst, id_iter_dst) {
if (GS(id_iter_dst->name) == ID_LI) {
/* Libraries need specific handling, as we want to check them by their filepath, not the IDs
* themselves. */
Library *lib_dst = reinterpret_cast<Library *>(id_iter_dst);
BLI_assert(!id_map_dst.contains(lib_dst->filepath_abs));
id_map_dst.add(lib_dst->filepath_abs, {id_iter_dst});
}
else {
id_map_dst.lookup_or_add(id_iter_dst->name, {}).append(id_iter_dst);
}
}
FOREACH_MAIN_ID_END;
/* Add the current `bmain_dst` filepath in the mapping as well, as it may be a library of the
* `bmain_src` Main. */
id_map_dst.add(bmain_dst->filepath, {nullptr});
/* A dedicated remapper for libraries is needed because these need to be remapped _before_ IDs
* are moved from `bmain_src` to `bmain_dst`, to avoid having to fix naming and ordering of IDs
* afterwards (especially in case some source linked IDs become local in `bmain_dst`). */
IDRemapper *id_remapper = BKE_id_remapper_create();
IDRemapper *id_remapper_libraries = BKE_id_remapper_create();
blender::Vector<ID *> ids_to_move;
FOREACH_MAIN_ID_BEGIN (bmain_src, id_iter_src) {
const bool is_library = GS(id_iter_src->name) == ID_LI;
blender::Vector<ID *> ids_dst = id_map_dst.lookup_default(
is_library ? reinterpret_cast<Library *>(id_iter_src)->filepath_abs : id_iter_src->name,
{});
if (is_library) {
BLI_assert(ids_dst.size() <= 1);
}
if (ids_dst.is_empty()) {
main_merge_add_id_to_move(
bmain_dst, id_map_dst, id_iter_src, id_remapper, ids_to_move, is_library, reports);
continue;
}
bool src_has_match_in_dst = false;
for (ID *id_iter_dst : ids_dst) {
if (are_ids_from_different_mains_matching(bmain_dst, id_iter_dst, bmain_src, id_iter_src)) {
/* There should only ever be one potential match, never more. */
BLI_assert(!src_has_match_in_dst);
if (!src_has_match_in_dst) {
if (is_library) {
BKE_id_remapper_add(id_remapper_libraries, id_iter_src, id_iter_dst);
reports.num_remapped_libraries++;
}
else {
BKE_id_remapper_add(id_remapper, id_iter_src, id_iter_dst);
reports.num_remapped_ids++;
}
src_has_match_in_dst = true;
}
#ifdef NDEBUG /* In DEBUG builds, keep looping to ensure there is only one match. */
break;
#endif
}
}
if (!src_has_match_in_dst) {
main_merge_add_id_to_move(
bmain_dst, id_map_dst, id_iter_src, id_remapper, ids_to_move, is_library, reports);
}
}
FOREACH_MAIN_ID_END;
reports.num_merged_ids = int(ids_to_move.size());
/* Rebase relative filepaths in `bmain_src` using `bmain_dst` path as new reference, or make them
* absolute if destination bmain has no filepath. */
if (bmain_src->filepath[0] != '\0') {
char dir_src[FILE_MAXDIR];
BLI_path_split_dir_part(bmain_src->filepath, dir_src, sizeof(dir_src));
BLI_path_normalize_native(dir_src);
if (bmain_dst->filepath[0] != '\0') {
char dir_dst[FILE_MAXDIR];
BLI_path_split_dir_part(bmain_dst->filepath, dir_dst, sizeof(dir_dst));
BLI_path_normalize_native(dir_dst);
BKE_bpath_relative_rebase(bmain_src, dir_src, dir_dst, reports.reports);
}
else {
BKE_bpath_absolute_convert(bmain_src, dir_src, reports.reports);
}
}
/* Libraries need to be remapped before moving IDs into `bmain_dst`, to ensure that the sorting
* of inserted IDs is correct. Note that no bmain is given here, so this is only a 'raw'
* remapping. */
BKE_libblock_relink_multiple(nullptr,
ids_to_move,
ID_REMAP_TYPE_REMAP,
id_remapper_libraries,
ID_REMAP_DO_LIBRARY_POINTERS);
for (ID *id_iter_src : ids_to_move) {
BKE_libblock_management_main_remove(bmain_src, id_iter_src);
BKE_libblock_management_main_add(bmain_dst, id_iter_src);
}
/* The other data has to be remapped once all IDs are in `bmain_dst`, to ensure that additional
* update process (e.g. collection hierarchy handling) happens as expected with the correct set
* of data. */
BKE_libblock_relink_multiple(bmain_dst, ids_to_move, ID_REMAP_TYPE_REMAP, id_remapper, 0);
BKE_reportf(
reports.reports,
RPT_INFO,
"Merged %d IDs from '%s' Main into '%s' Main; %d IDs and %d Libraries already existed as "
"part of the destination Main, and %d IDs missing from desination Main, were freed together "
"with the source Main",
reports.num_merged_ids,
bmain_src->filepath,
bmain_dst->filepath,
reports.num_remapped_ids,
reports.num_remapped_libraries,
reports.num_unknown_ids);
/* Remapping above may have made some IDs local. So namemap needs to be cleared, and moved IDs
* need to be re-sorted. */
BKE_main_namemap_clear(bmain_dst);
BLI_assert(BKE_main_namemap_validate(bmain_dst));
BKE_id_remapper_free(id_remapper);
BKE_id_remapper_free(id_remapper_libraries);
BKE_main_free(bmain_src);
*r_bmain_src = nullptr;
}
bool BKE_main_is_empty(Main *bmain)
{
bool result = true;

View File

@ -1,4 +1,4 @@
/* SPDX-FileCopyrightText: 2020 Blender Authors
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "testing/testing.h"
@ -8,14 +8,14 @@
#include "CLG_log.h"
#include "BLI_listbase.h"
#include "BLI_path_util.h"
#include "BLI_string.h"
#include "BKE_collection.h"
#include "BKE_idtype.h"
#include "BKE_lib_id.h"
#include "BKE_library.h"
#include "BKE_main.h"
#include "BKE_main_namemap.hh"
#include "BKE_main.hh"
#include "DNA_ID.h"
#include "DNA_collection_types.h"
@ -78,10 +78,15 @@ TEST_F(BMainMergeTest, basics)
EXPECT_EQ(1, BLI_listbase_count(&bmain_src->collections));
EXPECT_EQ(1, BLI_listbase_count(&bmain_src->objects));
BKE_main_merge(bmain_dst, &bmain_src, nullptr);
MainMergeReport reports = {};
BKE_main_merge(bmain_dst, &bmain_src, reports);
EXPECT_EQ(2, BLI_listbase_count(&bmain_dst->collections));
EXPECT_EQ(1, BLI_listbase_count(&bmain_dst->objects));
EXPECT_EQ(2, reports.num_merged_ids);
EXPECT_EQ(0, reports.num_unknown_ids);
EXPECT_EQ(0, reports.num_remapped_ids);
EXPECT_EQ(0, reports.num_remapped_libraries);
EXPECT_EQ(nullptr, bmain_src);
bmain_src = BKE_main_new();
@ -94,12 +99,17 @@ TEST_F(BMainMergeTest, basics)
EXPECT_EQ(1, BLI_listbase_count(&bmain_src->collections));
EXPECT_EQ(1, BLI_listbase_count(&bmain_src->objects));
BKE_main_merge(bmain_dst, &bmain_src, nullptr);
reports = {};
BKE_main_merge(bmain_dst, &bmain_src, reports);
/* The second `Ob_src` object in `bmain_src` cannot be merged in `bmain_dst`, since its name
* would collide with the first object. */
EXPECT_EQ(3, BLI_listbase_count(&bmain_dst->collections));
EXPECT_EQ(1, BLI_listbase_count(&bmain_dst->objects));
EXPECT_EQ(1, reports.num_merged_ids);
EXPECT_EQ(0, reports.num_unknown_ids);
EXPECT_EQ(1, reports.num_remapped_ids);
EXPECT_EQ(0, reports.num_remapped_libraries);
EXPECT_EQ(nullptr, bmain_src);
/* `Coll_src_2` should have been remapped to using `Ob_src` in `bmain_dst`, instead of `Ob_src`
@ -110,12 +120,18 @@ TEST_F(BMainMergeTest, basics)
TEST_F(BMainMergeTest, linked_data)
{
constexpr char *DST_PATH = "/tmp/dst/dst.blend";
constexpr char *SRC_PATH = "/tmp/src/src.blend";
constexpr char *LIB_PATH = "/tmp/lib/lib.blend";
#ifdef WIN32
# define ABS_ROOT "C:" SEP_STR
#else
# define ABS_ROOT SEP_STR
#endif
constexpr char DST_PATH[] = ABS_ROOT "tmp" SEP_STR "dst" SEP_STR "dst.blend";
constexpr char SRC_PATH[] = ABS_ROOT "tmp" SEP_STR "src" SEP_STR "src.blend";
constexpr char LIB_PATH[] = ABS_ROOT "tmp" SEP_STR "lib" SEP_STR "lib.blend";
constexpr char *LIB_PATH_RELATIVE = "//lib/lib.blend";
constexpr char *LIB_PATH_RELATIVE_ABS_SRC = "/tmp/src/lib/lib.blend";
constexpr char LIB_PATH_RELATIVE[] = "//lib" SEP_STR "lib.blend";
constexpr char LIB_PATH_RELATIVE_ABS_SRC[] = ABS_ROOT "tmp" SEP_STR "src" SEP_STR "lib" SEP_STR
"lib.blend";
EXPECT_TRUE(BLI_listbase_is_empty(&bmain_dst->libraries));
EXPECT_TRUE(BLI_listbase_is_empty(&bmain_dst->collections));
@ -144,7 +160,8 @@ TEST_F(BMainMergeTest, linked_data)
EXPECT_EQ(1, BLI_listbase_count(&bmain_src->objects));
EXPECT_EQ(1, BLI_listbase_count(&bmain_src->libraries));
BKE_main_merge(bmain_dst, &bmain_src, nullptr);
MainMergeReport reports = {};
BKE_main_merge(bmain_dst, &bmain_src, reports);
EXPECT_EQ(2, BLI_listbase_count(&bmain_dst->collections));
EXPECT_EQ(1, BLI_listbase_count(&bmain_dst->objects));
@ -152,6 +169,10 @@ TEST_F(BMainMergeTest, linked_data)
EXPECT_EQ(ob_1, bmain_dst->objects.first);
EXPECT_EQ(lib_src_1, bmain_dst->libraries.first);
EXPECT_EQ(ob_1->id.lib, lib_src_1);
EXPECT_EQ(3, reports.num_merged_ids);
EXPECT_EQ(0, reports.num_unknown_ids);
EXPECT_EQ(0, reports.num_remapped_ids);
EXPECT_EQ(0, reports.num_remapped_libraries);
EXPECT_EQ(nullptr, bmain_src);
/* Try another merge, with the same library path - second library should be skipped, destination
@ -172,7 +193,8 @@ TEST_F(BMainMergeTest, linked_data)
EXPECT_EQ(1, BLI_listbase_count(&bmain_src->objects));
EXPECT_EQ(1, BLI_listbase_count(&bmain_src->libraries));
BKE_main_merge(bmain_dst, &bmain_src, nullptr);
reports = {};
BKE_main_merge(bmain_dst, &bmain_src, reports);
EXPECT_EQ(3, BLI_listbase_count(&bmain_dst->collections));
EXPECT_EQ(2, BLI_listbase_count(&bmain_dst->objects));
@ -182,6 +204,10 @@ TEST_F(BMainMergeTest, linked_data)
EXPECT_EQ(lib_src_1, bmain_dst->libraries.first);
EXPECT_EQ(ob_1->id.lib, lib_src_1);
EXPECT_EQ(ob_2->id.lib, lib_src_1);
EXPECT_EQ(2, reports.num_merged_ids);
EXPECT_EQ(0, reports.num_unknown_ids);
EXPECT_EQ(0, reports.num_remapped_ids);
EXPECT_EQ(1, reports.num_remapped_libraries);
EXPECT_EQ(nullptr, bmain_src);
/* Use a relative library path. Since this is a different library, even though the object re-use
@ -203,7 +229,8 @@ TEST_F(BMainMergeTest, linked_data)
EXPECT_TRUE(STREQ(lib_src_3->filepath, LIB_PATH_RELATIVE));
EXPECT_TRUE(STREQ(lib_src_3->filepath_abs, LIB_PATH_RELATIVE_ABS_SRC));
BKE_main_merge(bmain_dst, &bmain_src, nullptr);
reports = {};
BKE_main_merge(bmain_dst, &bmain_src, reports);
EXPECT_EQ(4, BLI_listbase_count(&bmain_dst->collections));
EXPECT_EQ(3, BLI_listbase_count(&bmain_dst->objects));
@ -217,6 +244,10 @@ TEST_F(BMainMergeTest, linked_data)
EXPECT_EQ(ob_3->id.lib, lib_src_3);
EXPECT_FALSE(STREQ(lib_src_3->filepath, LIB_PATH_RELATIVE));
EXPECT_TRUE(STREQ(lib_src_3->filepath_abs, LIB_PATH_RELATIVE_ABS_SRC));
EXPECT_EQ(3, reports.num_merged_ids);
EXPECT_EQ(0, reports.num_unknown_ids);
EXPECT_EQ(0, reports.num_remapped_ids);
EXPECT_EQ(0, reports.num_remapped_libraries);
EXPECT_EQ(nullptr, bmain_src);
/* Try another merge, with the library path set to the path of the destination bmain. That source
@ -225,31 +256,35 @@ TEST_F(BMainMergeTest, linked_data)
bmain_src = BKE_main_new();
BLI_strncpy(bmain_src->filepath, SRC_PATH, sizeof(bmain_dst->filepath));
Collection *coll_4 = static_cast<Collection *>(BKE_id_new(bmain_src, ID_GR, "Coll_src_4"));
Collection *coll_4 = static_cast<Collection *>(BKE_id_new(bmain_src, ID_GR, "Coll_src"));
Object *ob_4 = static_cast<Object *>(BKE_id_new(bmain_src, ID_OB, "Ob_src_4"));
BKE_collection_object_add(bmain_src, coll_4, ob_4);
Library *lib_src_4 = static_cast<Library *>(BKE_id_new(bmain_src, ID_LI, DST_PATH));
BKE_library_filepath_set(bmain_src, lib_src_4, DST_PATH);
coll_4->id.lib = lib_src_4;
ob_4->id.lib = lib_src_4;
EXPECT_EQ(1, BLI_listbase_count(&bmain_src->collections));
EXPECT_EQ(1, BLI_listbase_count(&bmain_src->objects));
EXPECT_EQ(1, BLI_listbase_count(&bmain_src->libraries));
BKE_main_merge(bmain_dst, &bmain_src, nullptr);
reports = {};
BKE_main_merge(bmain_dst, &bmain_src, reports);
EXPECT_EQ(5, BLI_listbase_count(&bmain_dst->collections));
EXPECT_EQ(4, BLI_listbase_count(&bmain_dst->objects));
/* `bmain_dst` is unchanged, since both `coll_4` and `ob_4` were defined as linked from
* `bmain_dst`. */
EXPECT_EQ(4, BLI_listbase_count(&bmain_dst->collections));
EXPECT_EQ(3, BLI_listbase_count(&bmain_dst->objects));
EXPECT_EQ(2, BLI_listbase_count(&bmain_dst->libraries));
EXPECT_EQ(ob_1, bmain_dst->objects.first);
/* `ob_4` is now local in `bmain_dst`, so should come before linked ones. */
EXPECT_EQ(ob_4, ob_1->id.prev);
EXPECT_EQ(lib_src_3, bmain_dst->libraries.first);
EXPECT_EQ(lib_src_1, bmain_dst->libraries.last);
EXPECT_EQ(ob_1->id.lib, lib_src_1);
EXPECT_EQ(ob_2->id.lib, lib_src_1);
EXPECT_EQ(ob_3->id.lib, lib_src_3);
EXPECT_EQ(ob_4->id.lib, nullptr);
EXPECT_EQ(0, reports.num_merged_ids);
EXPECT_EQ(1, reports.num_unknown_ids);
EXPECT_EQ(1, reports.num_remapped_ids);
EXPECT_EQ(1, reports.num_remapped_libraries);
EXPECT_EQ(nullptr, bmain_src);
}