Sort blueprint plugins after all others

Also validate that no non-blueprint masters or non-masters are expected
to load after any blueprint masters.

The addition of a new EdgeType may be considered a breaking change to
the API, since it can result in the caller receiving a value it doesn't
know how to handle, but it doesn't break ABI compatibility. LOOT has
explicit handling for unknown values, so is fine.
This commit is contained in:
Oliver Hamlet
2024-08-24 16:23:30 +01:00
parent 828c673077
commit 6f4dff4766
7 changed files with 581 additions and 25 deletions
+1
View File
@@ -46,6 +46,7 @@ enum struct EdgeType : unsigned int {
recordOverlap,
assetOverlap,
tieBreak,
blueprintMaster,
};
}
+122 -19
View File
@@ -78,25 +78,40 @@ std::unordered_map<std::string, Group> GetGroupsMap(
return groupsMap;
}
bool IsInRange(const std::vector<PluginSortingData>::const_iterator& begin,
const std::vector<PluginSortingData>::const_iterator& end,
const std::string& name) {
return std::any_of(begin, end, [&](const PluginSortingData& plugin) {
return CompareFilenames(plugin.GetName(), name) == 0;
});
}
void ValidateSpecificAndHardcodedEdges(
const std::vector<PluginSortingData>::const_iterator& begin,
const std::vector<PluginSortingData>::const_iterator& firstBlueprintMaster,
const std::vector<PluginSortingData>::const_iterator& firstNonMaster,
const std::vector<PluginSortingData>::const_iterator& end,
const std::vector<std::string>& hardcodedPlugins) {
const auto isNonMaster = [&](const std::string& name) {
return std::any_of(
firstNonMaster, end, [&](const PluginSortingData& plugin) {
return CompareFilenames(plugin.GetName(), name) == 0;
});
return IsInRange(firstNonMaster, end, name);
};
const auto isBlueprintMaster = [&](const std::string& name) {
return IsInRange(firstBlueprintMaster, firstNonMaster, name);
};
for (auto it = begin; it != firstNonMaster; ++it) {
for (auto it = begin; it != firstBlueprintMaster; ++it) {
for (const auto& master : it->GetMasters()) {
if (isNonMaster(master)) {
throw CyclicInteractionError(
std::vector<Vertex>{Vertex(master, EdgeType::master),
Vertex(it->GetName(), EdgeType::masterFlag)});
}
if (isBlueprintMaster(master)) {
throw CyclicInteractionError(std::vector<Vertex>{
Vertex(master, EdgeType::master),
Vertex(it->GetName(), EdgeType::blueprintMaster)});
}
}
for (const auto& file : it->GetMasterlistRequirements()) {
@@ -106,6 +121,12 @@ void ValidateSpecificAndHardcodedEdges(
std::vector<Vertex>{Vertex(name, EdgeType::masterlistRequirement),
Vertex(it->GetName(), EdgeType::masterFlag)});
}
if (isBlueprintMaster(name)) {
throw CyclicInteractionError(std::vector<Vertex>{
Vertex(name, EdgeType::masterlistRequirement),
Vertex(it->GetName(), EdgeType::blueprintMaster)});
}
}
for (const auto& file : it->GetUserRequirements()) {
@@ -115,6 +136,12 @@ void ValidateSpecificAndHardcodedEdges(
std::vector<Vertex>{Vertex(name, EdgeType::userRequirement),
Vertex(it->GetName(), EdgeType::masterFlag)});
}
if (isBlueprintMaster(name)) {
throw CyclicInteractionError(std::vector<Vertex>{
Vertex(name, EdgeType::userRequirement),
Vertex(it->GetName(), EdgeType::blueprintMaster)});
}
}
for (const auto& file : it->GetMasterlistLoadAfterFiles()) {
@@ -124,6 +151,12 @@ void ValidateSpecificAndHardcodedEdges(
std::vector<Vertex>{Vertex(name, EdgeType::masterlistLoadAfter),
Vertex(it->GetName(), EdgeType::masterFlag)});
}
if (isBlueprintMaster(name)) {
throw CyclicInteractionError(std::vector<Vertex>{
Vertex(name, EdgeType::masterlistLoadAfter),
Vertex(it->GetName(), EdgeType::blueprintMaster)});
}
}
for (const auto& file : it->GetUserLoadAfterFiles()) {
@@ -133,6 +166,58 @@ void ValidateSpecificAndHardcodedEdges(
std::vector<Vertex>{Vertex(name, EdgeType::userLoadAfter),
Vertex(it->GetName(), EdgeType::masterFlag)});
}
if (isBlueprintMaster(name)) {
throw CyclicInteractionError(std::vector<Vertex>{
Vertex(name, EdgeType::userLoadAfter),
Vertex(it->GetName(), EdgeType::blueprintMaster)});
}
}
}
for (auto it = firstNonMaster; it != end; ++it) {
for (const auto& master : it->GetMasters()) {
if (isBlueprintMaster(master)) {
throw CyclicInteractionError(std::vector<Vertex>{
Vertex(master, EdgeType::master),
Vertex(it->GetName(), EdgeType::blueprintMaster)});
}
}
for (const auto& file : it->GetMasterlistRequirements()) {
const auto name = std::string(file.GetName());
if (isBlueprintMaster(name)) {
throw CyclicInteractionError(std::vector<Vertex>{
Vertex(name, EdgeType::masterlistRequirement),
Vertex(it->GetName(), EdgeType::blueprintMaster)});
}
}
for (const auto& file : it->GetUserRequirements()) {
const auto name = std::string(file.GetName());
if (isBlueprintMaster(name)) {
throw CyclicInteractionError(std::vector<Vertex>{
Vertex(name, EdgeType::userRequirement),
Vertex(it->GetName(), EdgeType::blueprintMaster)});
}
}
for (const auto& file : it->GetMasterlistLoadAfterFiles()) {
const auto name = std::string(file.GetName());
if (isBlueprintMaster(name)) {
throw CyclicInteractionError(std::vector<Vertex>{
Vertex(name, EdgeType::masterlistLoadAfter),
Vertex(it->GetName(), EdgeType::blueprintMaster)});
}
}
for (const auto& file : it->GetUserLoadAfterFiles()) {
const auto name = std::string(file.GetName());
if (isBlueprintMaster(name)) {
throw CyclicInteractionError(std::vector<Vertex>{
Vertex(name, EdgeType::userLoadAfter),
Vertex(it->GetName(), EdgeType::blueprintMaster)});
}
}
}
@@ -231,26 +316,41 @@ std::vector<std::string> SortPlugins(
// two thirds of all edges added. The cost of each bidirectional search
// scales with the number of edges, so reducing edges makes searches
// faster.
// As such, sort plugins using two separate graphs for masters and
// non-masters. This means that any edges that go from a non-master to a
// master are effectively ignored, so won't cause cyclic interaction errors.
// Edges going the other way will also effectively be ignored, but that
// shouldn't have a noticeable impact.
// Similarly, blueprint plugins load after all others.
// As such, sort plugins using three separate graphs for masters,
// non-masters and blueprint plugins. This means that any edges that go from a
// non-master to a master are effectively ignored, so won't cause cyclic
// interaction errors. Edges going the other way will also effectively be
// ignored, but that shouldn't have a noticeable impact.
const auto firstNonMasterIt = std::stable_partition(
pluginsSortingData.begin(),
pluginsSortingData.end(),
[](const PluginSortingData& plugin) { return plugin.IsMaster(); });
const auto firstBlueprintPluginIt =
std::stable_partition(pluginsSortingData.begin(),
firstNonMasterIt,
[](const PluginSortingData& plugin) {
return !plugin.IsBlueprintMaster();
});
ValidateSpecificAndHardcodedEdges(pluginsSortingData.begin(),
firstBlueprintPluginIt,
firstNonMasterIt,
pluginsSortingData.end(),
earlyLoadingPlugins);
auto newLoadOrder = SortPlugins(pluginsSortingData.begin(),
firstNonMasterIt,
earlyLoadingPlugins,
groupsMap,
predecessorGroupsMap);
auto newMastersLoadOrder = SortPlugins(pluginsSortingData.begin(),
firstBlueprintPluginIt,
earlyLoadingPlugins,
groupsMap,
predecessorGroupsMap);
const auto newBlueprintMastersLoadOrder = SortPlugins(firstBlueprintPluginIt,
firstNonMasterIt,
earlyLoadingPlugins,
groupsMap,
predecessorGroupsMap);
const auto newNonMastersLoadOrder = SortPlugins(firstNonMasterIt,
pluginsSortingData.end(),
@@ -258,11 +358,14 @@ std::vector<std::string> SortPlugins(
groupsMap,
predecessorGroupsMap);
newLoadOrder.insert(newLoadOrder.end(),
newNonMastersLoadOrder.begin(),
newNonMastersLoadOrder.end());
newMastersLoadOrder.insert(newMastersLoadOrder.end(),
newNonMastersLoadOrder.begin(),
newNonMastersLoadOrder.end());
newMastersLoadOrder.insert(newMastersLoadOrder.end(),
newBlueprintMastersLoadOrder.begin(),
newBlueprintMastersLoadOrder.end());
return newLoadOrder;
return newMastersLoadOrder;
}
std::vector<std::string> SortPlugins(
+5
View File
@@ -82,6 +82,11 @@ bool PluginSortingData::IsMaster() const {
return plugin_ != nullptr && plugin_->IsMaster();
}
bool PluginSortingData::IsBlueprintMaster() const {
return plugin_ != nullptr && plugin_->IsMaster() &&
plugin_->IsBlueprintPlugin();
}
bool PluginSortingData::LoadsArchive() const {
return plugin_ != nullptr && plugin_->LoadsArchive();
}
+5 -5
View File
@@ -40,14 +40,14 @@ public:
* PluginSortingData objects must not live longer than the Plugin objects
* that they are constructed from.
*/
explicit PluginSortingData(
const PluginSortingInterface* plugin,
const PluginMetadata& masterlistMetadata,
const PluginMetadata& userMetadata,
const std::vector<std::string>& loadOrder);
explicit PluginSortingData(const PluginSortingInterface* plugin,
const PluginMetadata& masterlistMetadata,
const PluginMetadata& userMetadata,
const std::vector<std::string>& loadOrder);
std::string GetName() const;
bool IsMaster() const;
bool IsBlueprintMaster() const;
bool LoadsArchive() const;
std::vector<std::string> GetMasters() const;
size_t GetOverrideRecordCount() const;
@@ -64,7 +64,7 @@ public:
bool IsUpdatePlugin() const override { return false; }
bool IsBlueprintPlugin() const override { return false; }
bool IsBlueprintPlugin() const override { return isBlueprintMaster_; }
bool IsValidAsLightPlugin() const override { return false; }
@@ -100,6 +100,10 @@ public:
void SetIsMaster(bool isMaster) { isMaster_ = isMaster; }
void SetIsBlueprintMaster(bool isBlueprintMaster) {
isBlueprintMaster_ = isBlueprintMaster;
}
void AddOverlappingRecords(const PluginInterface& plugin) {
recordsOverlapWith.insert(&plugin);
}
@@ -122,6 +126,7 @@ private:
size_t overrideRecordCount_{0};
size_t assetCount_{0};
bool isMaster_{false};
bool isBlueprintMaster_{false};
};
}
@@ -445,6 +445,33 @@ TEST_P(PluginSortTest,
EXPECT_EQ(expectedSortedOrder, sorted);
}
TEST_P(PluginSortTest, sortingShouldPutBlueprintPluginsAfterAllOthers) {
if (GetParam() != GameType::starfield) {
return;
}
SetBlueprintFlag(dataPath / blankEsm);
Game newGame(GetParam(), dataPath.parent_path(), localPath);
ASSERT_NO_THROW(loadInstalledPlugins(newGame, false));
std::vector<std::string> expectedSortedOrder({
masterFile,
blankDifferentEsm,
blankFullEsm,
blankMasterDependentEsm,
blankMediumEsm,
blankEsl,
blankEsp,
blankDifferentEsp,
blankMasterDependentEsp,
blankEsm,
});
const auto sorted = SortPlugins(newGame, newGame.GetLoadOrder());
EXPECT_EQ(expectedSortedOrder, sorted);
}
TEST_P(PluginSortTest, sortingShouldThrowIfACyclicInteractionIsEncountered) {
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
@@ -617,6 +644,382 @@ TEST_P(PluginSortTest,
e.GetCycle()[1].GetTypeOfEdgeToNextVertex());
}
}
TEST_P(PluginSortTest,
sortingShouldThrowIfAMasterEdgeWouldPutABlueprintMasterBeforeAMaster) {
if (GetParam() != GameType::starfield) {
return;
}
SetBlueprintFlag(dataPath / blankFullEsm);
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
try {
SortPlugins(game_, game_.GetLoadOrder());
FAIL();
} catch (const CyclicInteractionError& e) {
ASSERT_EQ(2, e.GetCycle().size());
EXPECT_EQ(blankFullEsm, e.GetCycle()[0].GetName());
EXPECT_EQ(EdgeType::master, e.GetCycle()[0].GetTypeOfEdgeToNextVertex());
EXPECT_EQ(blankMasterDependentEsm, e.GetCycle()[1].GetName());
EXPECT_EQ(EdgeType::blueprintMaster,
e.GetCycle()[1].GetTypeOfEdgeToNextVertex());
}
}
TEST_P(PluginSortTest,
sortingShouldThrowIfAMasterEdgeWouldPutABlueprintMasterBeforeANonMaster) {
if (GetParam() != GameType::starfield) {
return;
}
// Can't test with the test plugin files, so use the other SortPlugins()
// overload to provide stubs.
const auto esp = GetPlugin(blankMasterDependentEsp);
const auto blueprint = GetPlugin(blankFullEsm);
esp->AddMaster(blankFullEsm);
blueprint->SetIsMaster(true);
blueprint->SetIsBlueprintMaster(true);
std::vector<PluginSortingData> pluginsSortingData{
CreatePluginSortingData(esp->GetName()),
CreatePluginSortingData(blueprint->GetName())};
try {
SortPlugins(std::move(pluginsSortingData), {Group()}, {}, {});
FAIL();
} catch (const CyclicInteractionError& e) {
ASSERT_EQ(2, e.GetCycle().size());
EXPECT_EQ(blankFullEsm, e.GetCycle()[0].GetName());
EXPECT_EQ(EdgeType::master, e.GetCycle()[0].GetTypeOfEdgeToNextVertex());
EXPECT_EQ(blankMasterDependentEsp, e.GetCycle()[1].GetName());
EXPECT_EQ(EdgeType::blueprintMaster,
e.GetCycle()[1].GetTypeOfEdgeToNextVertex());
}
}
TEST_P(
PluginSortTest,
sortingShouldThrowIfAMasterlistRequirementEdgeWouldPutABlueprintMasterBeforeAMaster) {
using std::endl;
if (GetParam() != GameType::starfield) {
return;
}
SetBlueprintFlag(dataPath / blankDifferentEsm);
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
const auto masterlistPath = metadataFilesPath / "masterlist.yaml";
std::ofstream masterlist(masterlistPath);
masterlist << "plugins:" << endl
<< " - name: " << blankEsm << endl
<< " req:" << endl
<< " - " << blankDifferentEsm << endl;
masterlist.close();
game_.GetDatabase().LoadLists(masterlistPath);
try {
SortPlugins(game_, game_.GetLoadOrder());
FAIL();
} catch (const CyclicInteractionError& e) {
ASSERT_EQ(2, e.GetCycle().size());
EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName());
EXPECT_EQ(EdgeType::masterlistRequirement,
e.GetCycle()[0].GetTypeOfEdgeToNextVertex());
EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName());
EXPECT_EQ(EdgeType::blueprintMaster,
e.GetCycle()[1].GetTypeOfEdgeToNextVertex());
}
}
TEST_P(
PluginSortTest,
sortingShouldThrowIfAMasterlistRequirementEdgeWouldPutABlueprintMasterBeforeANonMaster) {
using std::endl;
if (GetParam() != GameType::starfield) {
return;
}
SetBlueprintFlag(dataPath / blankDifferentEsm);
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
const auto masterlistPath = metadataFilesPath / "masterlist.yaml";
std::ofstream masterlist(masterlistPath);
masterlist << "plugins:" << endl
<< " - name: " << blankEsp << endl
<< " req:" << endl
<< " - " << blankDifferentEsm << endl;
masterlist.close();
game_.GetDatabase().LoadLists(masterlistPath);
try {
SortPlugins(game_, game_.GetLoadOrder());
FAIL();
} catch (const CyclicInteractionError& e) {
ASSERT_EQ(2, e.GetCycle().size());
EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName());
EXPECT_EQ(EdgeType::masterlistRequirement,
e.GetCycle()[0].GetTypeOfEdgeToNextVertex());
EXPECT_EQ(blankEsp, e.GetCycle()[1].GetName());
EXPECT_EQ(EdgeType::blueprintMaster,
e.GetCycle()[1].GetTypeOfEdgeToNextVertex());
}
}
TEST_P(
PluginSortTest,
sortingShouldThrowIfAUserRequirementEdgeWouldPutABlueprintMasterBeforeAMaster) {
if (GetParam() != GameType::starfield) {
return;
}
SetBlueprintFlag(dataPath / blankDifferentEsm);
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
PluginMetadata plugin(blankEsm);
plugin.SetRequirements({File(blankDifferentEsm)});
game_.GetDatabase().SetPluginUserMetadata(plugin);
try {
SortPlugins(game_, game_.GetLoadOrder());
FAIL();
} catch (const CyclicInteractionError& e) {
ASSERT_EQ(2, e.GetCycle().size());
EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName());
EXPECT_EQ(EdgeType::userRequirement,
e.GetCycle()[0].GetTypeOfEdgeToNextVertex());
EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName());
EXPECT_EQ(EdgeType::blueprintMaster,
e.GetCycle()[1].GetTypeOfEdgeToNextVertex());
}
}
TEST_P(
PluginSortTest,
sortingShouldThrowIfAUserRequirementEdgeWouldPutABlueprintMasterBeforeANonMaster) {
if (GetParam() != GameType::starfield) {
return;
}
SetBlueprintFlag(dataPath / blankDifferentEsm);
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
PluginMetadata plugin(blankEsp);
plugin.SetRequirements({File(blankDifferentEsm)});
game_.GetDatabase().SetPluginUserMetadata(plugin);
try {
SortPlugins(game_, game_.GetLoadOrder());
FAIL();
} catch (const CyclicInteractionError& e) {
ASSERT_EQ(2, e.GetCycle().size());
EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName());
EXPECT_EQ(EdgeType::userRequirement,
e.GetCycle()[0].GetTypeOfEdgeToNextVertex());
EXPECT_EQ(blankEsp, e.GetCycle()[1].GetName());
EXPECT_EQ(EdgeType::blueprintMaster,
e.GetCycle()[1].GetTypeOfEdgeToNextVertex());
}
}
TEST_P(
PluginSortTest,
sortingShouldThrowIfAMasterlistLoadAfterEdgeWouldPutABlueprintMasterBeforeAMaster) {
using std::endl;
if (GetParam() != GameType::starfield) {
return;
}
SetBlueprintFlag(dataPath / blankDifferentEsm);
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
const auto masterlistPath = metadataFilesPath / "masterlist.yaml";
std::ofstream masterlist(masterlistPath);
masterlist << "plugins:" << endl
<< " - name: " << blankEsm << endl
<< " after:" << endl
<< " - " << blankDifferentEsm << endl;
masterlist.close();
game_.GetDatabase().LoadLists(masterlistPath);
try {
SortPlugins(game_, game_.GetLoadOrder());
FAIL();
} catch (const CyclicInteractionError& e) {
ASSERT_EQ(2, e.GetCycle().size());
EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName());
EXPECT_EQ(EdgeType::masterlistLoadAfter,
e.GetCycle()[0].GetTypeOfEdgeToNextVertex());
EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName());
EXPECT_EQ(EdgeType::blueprintMaster,
e.GetCycle()[1].GetTypeOfEdgeToNextVertex());
}
}
TEST_P(
PluginSortTest,
sortingShouldThrowIfAMasterlistLoadAfterEdgeWouldPutABlueprintMasterBeforeANonMaster) {
using std::endl;
if (GetParam() != GameType::starfield) {
return;
}
SetBlueprintFlag(dataPath / blankDifferentEsm);
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
const auto masterlistPath = metadataFilesPath / "masterlist.yaml";
std::ofstream masterlist(masterlistPath);
masterlist << "plugins:" << endl
<< " - name: " << blankEsp << endl
<< " after:" << endl
<< " - " << blankDifferentEsm << endl;
masterlist.close();
game_.GetDatabase().LoadLists(masterlistPath);
try {
SortPlugins(game_, game_.GetLoadOrder());
FAIL();
} catch (const CyclicInteractionError& e) {
ASSERT_EQ(2, e.GetCycle().size());
EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName());
EXPECT_EQ(EdgeType::masterlistLoadAfter,
e.GetCycle()[0].GetTypeOfEdgeToNextVertex());
EXPECT_EQ(blankEsp, e.GetCycle()[1].GetName());
EXPECT_EQ(EdgeType::blueprintMaster,
e.GetCycle()[1].GetTypeOfEdgeToNextVertex());
}
}
TEST_P(
PluginSortTest,
sortingShouldThrowIfAUserLoadAfterEdgeWouldPutABlueprintMasterBeforeAMaster) {
if (GetParam() != GameType::starfield) {
return;
}
SetBlueprintFlag(dataPath / blankDifferentEsm);
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
PluginMetadata plugin(blankEsm);
plugin.SetLoadAfterFiles({File(blankDifferentEsm)});
game_.GetDatabase().SetPluginUserMetadata(plugin);
try {
SortPlugins(game_, game_.GetLoadOrder());
FAIL();
} catch (const CyclicInteractionError& e) {
ASSERT_EQ(2, e.GetCycle().size());
EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName());
EXPECT_EQ(EdgeType::userLoadAfter,
e.GetCycle()[0].GetTypeOfEdgeToNextVertex());
EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName());
EXPECT_EQ(EdgeType::blueprintMaster,
e.GetCycle()[1].GetTypeOfEdgeToNextVertex());
}
}
TEST_P(
PluginSortTest,
sortingShouldThrowIfAUserLoadAfterEdgeWouldPutABlueprintMasterBeforeANonMaster) {
if (GetParam() != GameType::starfield) {
return;
}
SetBlueprintFlag(dataPath / blankDifferentEsm);
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
PluginMetadata plugin(blankEsp);
plugin.SetLoadAfterFiles({File(blankDifferentEsm)});
game_.GetDatabase().SetPluginUserMetadata(plugin);
try {
SortPlugins(game_, game_.GetLoadOrder());
FAIL();
} catch (const CyclicInteractionError& e) {
ASSERT_EQ(2, e.GetCycle().size());
EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName());
EXPECT_EQ(EdgeType::userLoadAfter,
e.GetCycle()[0].GetTypeOfEdgeToNextVertex());
EXPECT_EQ(blankEsp, e.GetCycle()[1].GetName());
EXPECT_EQ(EdgeType::blueprintMaster,
e.GetCycle()[1].GetTypeOfEdgeToNextVertex());
}
}
TEST_P(
PluginSortTest,
sortingShouldNotThrowIfAHardcodedEdgeWouldPutABlueprintMasterBeforeAMaster) {
if (GetParam() != GameType::starfield) {
return;
}
// Can't test with the test plugin files, so use the other SortPlugins()
// overload to provide stubs.
const auto esm = GetPlugin(blankEsm);
const auto blueprint = GetPlugin(blankDifferentEsm);
esm->SetIsMaster(true);
blueprint->SetIsMaster(true);
blueprint->SetIsBlueprintMaster(true);
std::vector<PluginSortingData> pluginsSortingData{
CreatePluginSortingData(esm->GetName()),
CreatePluginSortingData(blueprint->GetName())};
const auto sorted = SortPlugins(
std::move(pluginsSortingData), {Group()}, {}, {blueprint->GetName()});
EXPECT_EQ(std::vector<std::string>({
blankEsm, blankDifferentEsm,
}),
sorted);
}
TEST_P(
PluginSortTest,
sortingShouldNotThrowIfAHardcodedEdgeWouldPutABlueprintMasterBeforeANonMaster) {
if (GetParam() != GameType::starfield) {
return;
}
// Can't test with the test plugin files, so use the other SortPlugins()
// overload to provide stubs.
const auto esm = GetPlugin(blankEsp);
const auto blueprint = GetPlugin(blankDifferentEsm);
esm->SetIsMaster(true);
blueprint->SetIsMaster(true);
blueprint->SetIsBlueprintMaster(true);
std::vector<PluginSortingData> pluginsSortingData{
CreatePluginSortingData(esm->GetName()),
CreatePluginSortingData(blueprint->GetName())};
const auto sorted = SortPlugins(
std::move(pluginsSortingData), {Group()}, {}, {blueprint->GetName()});
EXPECT_EQ(std::vector<std::string>({
blankEsp,
blankDifferentEsm,
}),
sorted);
}
}
}
@@ -132,6 +132,45 @@ TEST_P(PluginSortingDataTest,
EXPECT_EQ(4, plugin.GetOverrideRecordCount());
}
}
TEST_P(PluginSortingDataTest,
isBlueprintMasterShouldBeTrueIfPluginIsAMasterAndABlueprintPlugin) {
SetBlueprintFlag(dataPath / blankEsm);
SetBlueprintFlag(dataPath / blankEsp);
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
auto plugin = PluginSortingData(
dynamic_cast<const Plugin *>(game_.GetPlugin(blankEsm)),
PluginMetadata(),
PluginMetadata(),
getLoadOrder());
if (GetParam() == GameType::starfield) {
EXPECT_TRUE(plugin.IsBlueprintMaster());
} else {
EXPECT_FALSE(plugin.IsBlueprintMaster());
}
plugin = PluginSortingData(
dynamic_cast<const Plugin *>(game_.GetPlugin(blankDifferentEsm)),
PluginMetadata(),
PluginMetadata(),
getLoadOrder());
EXPECT_FALSE(plugin.IsBlueprintMaster());
plugin = PluginSortingData(dynamic_cast<const Plugin *>(game_.GetPlugin(blankEsp)),
PluginMetadata(),
PluginMetadata(),
getLoadOrder());
EXPECT_FALSE(plugin.IsBlueprintMaster());
plugin = PluginSortingData(
dynamic_cast<const Plugin *>(game_.GetPlugin(blankDifferentEsp)),
PluginMetadata(),
PluginMetadata(),
getLoadOrder());
EXPECT_FALSE(plugin.IsBlueprintMaster());
}
}
}