#include "catch.hpp" #include "macro-test-helpers.hpp" #include #include #include namespace { using namespace advss; using namespace advss::test; using Names = std::vector; Names names(const std::vector> ¯os) { Names result; for (const auto ¯o : macros) { result.emplace_back(macro->Name()); } return result; } uint32_t groupSize(const std::string &name) { auto group = GetMacro(name); REQUIRE(group); REQUIRE(group->IsGroup()); return group->GroupSize(); } void loadFromJson(const char *json) { OBSDataAutoRelease data = obs_data_create_from_json(json); REQUIRE(data); LoadMacros(data); } void saveAndLoad() { OBSDataAutoRelease data = obs_data_create(); SaveMacros(data); GetTopLevelMacros().clear(); LoadMacros(data); } struct ClearMacros { ~ClearMacros() { GetTopLevelMacros().clear(); } }; } // namespace TEST_CASE("Nested group sizes", "[macro-group]") { ClearMacros clear; SetupMacros("A, G1[B, G2[C, D], E], F"); REQUIRE(DescribeMacros() == "A, G1[B, G2[C, D], E], F"); REQUIRE(groupSize("G1") == 5); REQUIRE(groupSize("G2") == 2); REQUIRE(GetMacro("C")->Parent() == GetMacro("G2")); REQUIRE(GetMacro("G2")->Parent() == GetMacro("G1")); REQUIRE_FALSE(GetMacro("A")->Parent()); } TEST_CASE("Group helpers", "[macro-group]") { ClearMacros clear; SetupMacros("A, G1[B, G2[C, D], E, G3[]], F"); auto g1 = GetMacro("G1").get(); auto g2 = GetMacro("G2").get(); REQUIRE(names(GetGroupChildren(g1)) == Names{"B", "G2", "E", "G3"}); REQUIRE(names(GetGroupDescendants(g1)) == Names{"B", "G2", "C", "D", "E", "G3"}); REQUIRE(names(GetGroupMacroEntries(g1)) == Names{"B", "C", "D", "E"}); REQUIRE(names(GetGroupMacroEntries(g2)) == Names{"C", "D"}); REQUIRE(GetGroupMacroEntries(GetMacro("G3").get()).empty()); REQUIRE(GetGroupChildren(GetMacro("A").get()).empty()); REQUIRE(IsMacroInGroup(GetMacro("C").get(), g1)); REQUIRE(IsMacroInGroup(GetMacro("C").get(), g2)); REQUIRE_FALSE(IsMacroInGroup(GetMacro("B").get(), g2)); REQUIRE_FALSE(IsMacroInGroup(g1, g1)); REQUIRE_FALSE(IsMacroInGroup(GetMacro("A").get(), g1)); } TEST_CASE("Move macros between nested groups", "[macro-group]") { ClearMacros clear; SetupMacros("A, G1[B, G2[C, D], E], F"); SECTION("Group with children to top level") { Macro::PrepareMoveToGroup(nullptr, GetMacro("G2")); REQUIRE(groupSize("G1") == 2); REQUIRE(groupSize("G2") == 2); REQUIRE_FALSE(GetMacro("G2")->Parent()); } SECTION("Macro into nested group") { Macro::PrepareMoveToGroup(GetMacro("G2"), GetMacro("A")); REQUIRE(groupSize("G1") == 6); REQUIRE(groupSize("G2") == 3); REQUIRE(GetMacro("A")->Parent() == GetMacro("G2")); } SECTION("Macro from nested group to outer group") { Macro::PrepareMoveToGroup(GetMacro("G1"), GetMacro("C")); REQUIRE(groupSize("G1") == 5); REQUIRE(groupSize("G2") == 1); } SECTION("Group into itself is rejected") { Macro::PrepareMoveToGroup(GetMacro("G1"), GetMacro("G1")); REQUIRE_FALSE(GetMacro("G1")->Parent()); REQUIRE(groupSize("G1") == 5); } SECTION("Group into own descendant is rejected") { Macro::PrepareMoveToGroup(GetMacro("G2"), GetMacro("G1")); REQUIRE_FALSE(GetMacro("G1")->Parent()); REQUIRE(groupSize("G1") == 5); REQUIRE(groupSize("G2") == 2); } } TEST_CASE("Create group from nested macros", "[macro-group]") { ClearMacros clear; SetupMacros("A, G1[B, C, D]"); std::vector> children = {GetMacro("B"), GetMacro("C")}; auto group = Macro::CreateGroup("G2", children); REQUIRE(group->Parent() == GetMacro("G1")); REQUIRE(group->GroupSize() == 2); REQUIRE(groupSize("G1") == 4); REQUIRE(GetMacro("B")->Parent() == group); } TEST_CASE("Remove nested group", "[macro-group]") { ClearMacros clear; SetupMacros("A, G1[B, G2[C, D], E], F"); SECTION("Inner group") { Macro::RemoveGroup(GetMacro("G2")); REQUIRE(DescribeMacros() == "A, G1[B, C, D, E], F"); REQUIRE(groupSize("G1") == 4); } SECTION("Outer group") { Macro::RemoveGroup(GetMacro("G1")); REQUIRE(DescribeMacros() == "A, B, G2[C, D], E, F"); REQUIRE(groupSize("G2") == 2); } } TEST_CASE("Save and load nested groups", "[macro-group]") { ClearMacros clear; SetupMacros("A, G1{B, G2[C, G3{D}], E, G4[]}, F"); saveAndLoad(); REQUIRE(DescribeMacros() == "A, G1{B, G2[C, G3{D}], E, G4[]}, F"); REQUIRE(groupSize("G1") == 7); REQUIRE(groupSize("G2") == 3); REQUIRE(groupSize("G3") == 1); REQUIRE(groupSize("G4") == 0); } TEST_CASE("Saved group size is number of descendants", "[macro-group]") { ClearMacros clear; SetupMacros("G1[A, G2[B, C]]"); OBSDataAutoRelease data = obs_data_create(); SaveMacros(data); OBSDataArrayAutoRelease array = obs_data_get_array(data, "macros"); REQUIRE(obs_data_array_count(array) == 5); OBSDataAutoRelease g1 = obs_data_array_item(array, 0); OBSDataAutoRelease g1Data = obs_data_get_obj(g1, "groupData"); REQUIRE(obs_data_get_int(g1Data, "size") == 4); OBSDataAutoRelease g2 = obs_data_array_item(array, 2); OBSDataAutoRelease g2Data = obs_data_get_obj(g2, "groupData"); REQUIRE(obs_data_get_int(g2Data, "size") == 2); } TEST_CASE("Load single level groups", "[macro-group]") { ClearMacros clear; loadFromJson(R"({"macros": [ {"name": "A"}, {"name": "G1", "group": true, "groupData": {"size": 2, "collapsed": true}}, {"name": "B"}, {"name": "C"}, {"name": "D"} ]})"); REQUIRE(DescribeMacros() == "A, G1{B, C}, D"); REQUIRE(groupSize("G1") == 2); } TEST_CASE("Load groups with invalid sizes", "[macro-group]") { ClearMacros clear; SECTION("Group size exceeds number of macros") { loadFromJson(R"({"macros": [ {"name": "A"}, {"name": "G1", "group": true, "groupData": {"size": 5}}, {"name": "B"}, {"name": "C"} ]})"); REQUIRE(DescribeMacros() == "A, G1[B, C]"); REQUIRE(groupSize("G1") == 2); } SECTION("Nested group exceeds parent group") { loadFromJson(R"({"macros": [ {"name": "G1", "group": true, "groupData": {"size": 2}}, {"name": "G2", "group": true, "groupData": {"size": 3}}, {"name": "A"}, {"name": "B"}, {"name": "C"} ]})"); REQUIRE(DescribeMacros() == "G1[G2[A]], B, C"); REQUIRE(groupSize("G1") == 2); REQUIRE(groupSize("G2") == 1); } SECTION("Empty group at the end") { loadFromJson(R"({"macros": [ {"name": "A"}, {"name": "G1", "group": true, "groupData": {"size": 1}} ]})"); REQUIRE(DescribeMacros() == "A, G1[]"); } } TEST_CASE("Execution time is propagated to all parent groups", "[macro-group]") { ClearMacros clear; SetupMacros("G1[G2[A]], B"); const auto start = std::chrono::high_resolution_clock::now(); GetMacro("A")->PerformActions(true); REQUIRE(GetMacro("G1")->WasExecutedSince(start)); REQUIRE(GetMacro("G2")->WasExecutedSince(start)); REQUIRE_FALSE(GetMacro("B")->WasExecutedSince(start)); } namespace { void checkGroupSizes() { const auto ¯os = GetTopLevelMacros(); for (const auto ¯o : macros) { if (!macro->IsGroup()) { continue; } INFO("group " << macro->Name() << " in " << DescribeMacros()); const auto descendants = std::count_if( macros.begin(), macros.end(), [¯o](const std::shared_ptr &m) { return IsMacroInGroup(m.get(), macro.get()); }); REQUIRE(macro->GroupSize() == (uint32_t)descendants); } } std::vector> macros(const Names &names) { std::vector> result; for (const auto &name : names) { result.emplace_back(GetMacro(name)); } return result; } int indexOf(const std::string &name) { const auto &all = GetTopLevelMacros(); for (int i = 0; i < (int)all.size(); i++) { if (all[i]->Name() == name) { return i; } } return -1; } // Mirrors AdvSceneSwitcher::ExportMacros() OBSDataArrayAutoRelease exportMacros(const Names &selection) { OBSDataArrayAutoRelease array = obs_data_array_create(); for (const auto ¯o : GetMacrosWithGroupContents(macros(selection))) { OBSDataAutoRelease obj = obs_data_create(); macro->Save(obj); obs_data_array_push_back(array, obj); } return array; } } // namespace TEST_CASE("Outermost macros", "[macro-group]") { ClearMacros clear; SetupMacros("A, G1[B, G2[C, D], E], F"); REQUIRE(names(GetOutermostMacros(macros({"C", "G2", "F", "A"}))) == Names{"A", "G2", "F"}); REQUIRE(names(GetOutermostMacros(macros({"C", "G2", "G1"}))) == Names{"G1"}); REQUIRE(GetOutermostMacros({}).empty()); } TEST_CASE("Macros with group contents", "[macro-group]") { ClearMacros clear; SetupMacros("A, G1[B, G2[C, D], E], F"); REQUIRE(names(GetMacrosWithGroupContents(macros({"F", "G2", "A"}))) == Names{"A", "G2", "C", "D", "F"}); REQUIRE(names(GetMacrosWithGroupContents(macros({"C", "G1"}))) == Names{"G1", "B", "G2", "C", "D", "E"}); } TEST_CASE("Remove and restore macro subtrees", "[macro-group]") { ClearMacros clear; SetupMacros("A, G1[B, G2[C, D], E], F"); SECTION("Macro in nested group") { auto macro = GetMacro("C"); const int index = indexOf("C"); RemoveMacroSubtree(macro); REQUIRE(DescribeMacros() == "A, G1[B, G2[D], E], F"); checkGroupSizes(); InsertMacroSubtree(index, {macro}, GetMacro("G2")); REQUIRE(DescribeMacros() == "A, G1[B, G2[C, D], E], F"); checkGroupSizes(); } SECTION("Nested group") { const std::deque> subtree = { GetMacro("G2"), GetMacro("C"), GetMacro("D")}; const int index = indexOf("G2"); RemoveMacroSubtree(GetMacro("G2")); REQUIRE(DescribeMacros() == "A, G1[B, E], F"); checkGroupSizes(); InsertMacroSubtree(index, subtree, GetMacro("G1")); REQUIRE(DescribeMacros() == "A, G1[B, G2[C, D], E], F"); checkGroupSizes(); } // Mirrors the group delete undo, which saves and loads the group and // its contents SECTION("Group from saved data") { auto group = GetMacro("G1"); const int index = indexOf("G1"); OBSDataArrayAutoRelease array = exportMacros({"G1"}); RemoveMacroSubtree(group); REQUIRE(DescribeMacros() == "A, F"); std::deque> subtree; for (size_t i = 0; i < obs_data_array_count(array); i++) { OBSDataAutoRelease obj = obs_data_array_item(array, i); auto macro = std::make_shared(); macro->Load(obj); subtree.emplace_back(macro); } Macro::RestoreGroupHierarchy(subtree); InsertMacroSubtree(index, subtree, nullptr); REQUIRE(DescribeMacros() == "A, G1[B, G2[C, D], E], F"); checkGroupSizes(); } } TEST_CASE("Ungroup and regroup", "[macro-group]") { ClearMacros clear; SECTION("Nested group") { SetupMacros("A, G1[B, G2[C, D], E], F"); const int index = indexOf("G2"); auto children = GetGroupChildren(GetMacro("G2").get()); Macro::RemoveGroup(GetMacro("G2")); REQUIRE(DescribeMacros() == "A, G1[B, C, D, E], F"); InsertGroup("G2", index, children, GetMacro("G1")); REQUIRE(DescribeMacros() == "A, G1[B, G2[C, D], E], F"); checkGroupSizes(); } SECTION("Group containing groups") { SetupMacros("A, G1[B, G2[C, G3[D]], E], F"); const int index = indexOf("G1"); auto children = GetGroupChildren(GetMacro("G1").get()); Macro::RemoveGroup(GetMacro("G1")); REQUIRE(DescribeMacros() == "A, B, G2[C, G3[D]], E, F"); InsertGroup("G1", index, children, nullptr); REQUIRE(DescribeMacros() == "A, G1[B, G2[C, G3[D]], E], F"); checkGroupSizes(); } SECTION("Empty nested group") { SetupMacros("A, G1[G2[]]"); const int index = indexOf("G2"); std::vector> children; Macro::RemoveGroup(GetMacro("G2")); REQUIRE(DescribeMacros() == "A, G1[]"); InsertGroup("G2", index, children, GetMacro("G1")); REQUIRE(DescribeMacros() == "A, G1[G2[]]"); checkGroupSizes(); } } TEST_CASE("Export and import nested groups", "[macro-group]") { ClearMacros clear; SetupMacros("A, G1[B, G2{C, D}], E"); OBSDataArrayAutoRelease array = exportMacros({"G1", "C"}); const auto importAll = [](const std::shared_ptr &) { return true; }; SECTION("Into empty macro list") { GetTopLevelMacros().clear(); const auto imported = LoadImportedMacros(array, importAll); REQUIRE(names(imported) == Names{"G1", "B", "G2", "C", "D"}); REQUIRE(DescribeMacros() == "G1[B, G2{C, D}]"); checkGroupSizes(); } // Mirrors AdvSceneSwitcher::ImportMacros() skipping macros for which // the name conflict was not resolved const auto renameOrSkip = [](const std::string &skip) { return [skip](const std::shared_ptr ¯o) { if (macro->Name() == skip) { return false; } macro->SetName(macro->Name() + "x"); return true; }; }; SECTION("Skip outer group") { LoadImportedMacros(array, renameOrSkip("G1")); REQUIRE(DescribeMacros() == "A, G1[B, G2{C, D}], E, Bx, G2x{Cx, Dx}"); checkGroupSizes(); } SECTION("Skip inner group") { LoadImportedMacros(array, renameOrSkip("G2")); REQUIRE(DescribeMacros() == "A, G1[B, G2{C, D}], E, G1x[Bx, Cx, Dx]"); checkGroupSizes(); } SECTION("Skip macro in nested group") { LoadImportedMacros(array, renameOrSkip("C")); REQUIRE(DescribeMacros() == "A, G1[B, G2{C, D}], E, G1x[Bx, G2x{Dx}]"); checkGroupSizes(); } } TEST_CASE("Import single level groups", "[macro-group]") { ClearMacros clear; OBSDataAutoRelease data = obs_data_create_from_json(R"({"macros": [ {"name": "G1", "group": true, "groupData": {"size": 2, "collapsed": true}}, {"name": "A"}, {"name": "B"}, {"name": "C"} ]})"); OBSDataArrayAutoRelease array = obs_data_get_array(data, "macros"); LoadImportedMacros(array, [](const std::shared_ptr &) { return true; }); REQUIRE(DescribeMacros() == "G1{A, B}, C"); checkGroupSizes(); }