#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "apps/openmw/mwworld/esmstore.hpp" static Loading::Listener dummyListener; /// Base class for tests of ESMStore that rely on external content files to produce the test results struct ContentFileTest : public ::testing::Test { protected: void SetUp() override { readContentFiles(); // load the content files int index = 0; ESM::Dialogue* dialogue = nullptr; for (const auto& mContentFile : mContentFiles) { ESM::ESMReader lEsm; lEsm.setEncoder(nullptr); lEsm.setIndex(index); lEsm.open(mContentFile); mEsmStore.load(lEsm, &dummyListener, dialogue); ++index; } mEsmStore.setUp(); } void TearDown() override {} // read absolute path to content files from openmw.cfg void readContentFiles() { boost::program_options::variables_map variables; boost::program_options::options_description desc("Allowed options"); auto addOption = desc.add_options(); addOption("data", boost::program_options::value() ->default_value(Files::MaybeQuotedPathContainer(), "data") ->multitoken() ->composing()); addOption("content", boost::program_options::value>() ->default_value(std::vector(), "") ->multitoken() ->composing(), "content file(s): esm/esp, or omwgame/omwaddon"); addOption("data-local", boost::program_options::value()->default_value( Files::MaybeQuotedPathContainer::value_type(), "")); Files::ConfigurationManager::addCommonOptions(desc); mConfigurationManager.readConfiguration(variables, desc, true); Files::PathContainer dataDirs, dataLocal; if (!variables["data"].empty()) { dataDirs = asPathContainer(variables["data"].as()); } Files::PathContainer::value_type local( variables["data-local"].as().u8string()); if (!local.empty()) dataLocal.push_back(local); mConfigurationManager.filterOutNonExistingPaths(dataDirs); mConfigurationManager.filterOutNonExistingPaths(dataLocal); if (!dataLocal.empty()) dataDirs.insert(dataDirs.end(), dataLocal.begin(), dataLocal.end()); Files::Collections collections(dataDirs); std::vector contentFiles = variables["content"].as>(); for (auto& contentFile : contentFiles) { if (!Misc::StringUtils::ciEndsWith(contentFile, ".omwscripts")) mContentFiles.push_back(collections.getPath(contentFile)); } } protected: Files::ConfigurationManager mConfigurationManager; MWWorld::ESMStore mEsmStore; std::vector mContentFiles; }; /// Print results of the dialogue merging process, i.e. the resulting linked list. TEST_F(ContentFileTest, dialogue_merging_test) { if (mContentFiles.empty()) { std::cout << "No content files found, skipping test" << std::endl; return; } const auto file = TestingOpenMW::outputFilePath("test_dialogue_merging.txt"); std::ofstream stream(file); const MWWorld::Store& dialStore = mEsmStore.get(); for (const auto& dial : dialStore) { stream << "Dialogue: " << dial.mId << std::endl; for (const auto& info : dial.mInfo) { stream << info.mId << std::endl; } stream << std::endl; } std::cout << "dialogue_merging_test successful, results printed to " << Files::pathToUnicodeString(file) << std::endl; } // Note: here we don't test records that don't use string names (e.g. Land, Pathgrid, Cell) #define RUN_TEST_FOR_TYPES(func, arg1, arg2) \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); \ func(arg1, arg2); template void printRecords(MWWorld::ESMStore& esmStore, std::ostream& outStream) { const MWWorld::Store& store = esmStore.get(); outStream << store.getSize() << " " << T::getRecordType() << " records" << std::endl; for (typename MWWorld::Store::iterator it = store.begin(); it != store.end(); ++it) { const T& record = *it; outStream << record.mId << std::endl; } outStream << std::endl; } /// Print some basic diagnostics about the loaded content files, e.g. number of records and names of those records /// Also used to test the iteration order of records TEST_F(ContentFileTest, content_diagnostics_test) { if (mContentFiles.empty()) { std::cout << "No content files found, skipping test" << std::endl; return; } const auto file = TestingOpenMW::outputFilePath("test_content_diagnostics.txt"); std::ofstream stream(file); RUN_TEST_FOR_TYPES(printRecords, mEsmStore, stream); std::cout << "diagnostics_test successful, results printed to " << Files::pathToUnicodeString(file) << std::endl; } // TODO: /// Print results of autocalculated NPC spell lists. Also serves as test for attribute/skill autocalculation which the /// spell autocalculation heavily relies on /// - even incorrect rounding modes can completely change the resulting spell lists. /* TEST_F(ContentFileTest, autocalc_test) { if (mContentFiles.empty()) { std::cout << "No content files found, skipping test" << std::endl; return; } } */ /// Base class for tests of ESMStore that do not rely on external content files template struct StoreTest : public ::testing::Test { }; TYPED_TEST_SUITE_P(StoreTest); /// Create an ESM file in-memory containing the specified record. /// @param deleted Write record with deleted flag? template std::unique_ptr getEsmFile(T record, bool deleted, ESM::FormatVersion formatVersion) { ESM::ESMWriter writer; auto stream = std::make_unique(); writer.setFormatVersion(formatVersion); writer.save(*stream); writer.startRecord(T::sRecordId); record.save(writer, deleted); writer.endRecord(T::sRecordId); return stream; } namespace { std::vector getFormats() { std::vector result({ ESM::DefaultFormatVersion, ESM::CurrentContentFormatVersion, ESM::MaxOldFogOfWarFormatVersion, ESM::MaxUnoptimizedCharacterDataFormatVersion, ESM::MaxOldTimeLeftFormatVersion, ESM::MaxIntFallbackFormatVersion, ESM::MaxClearModifiersFormatVersion, ESM::MaxOldAiPackageFormatVersion, ESM::MaxOldSkillsAndAttributesFormatVersion, ESM::MaxOldCreatureStatsFormatVersion, ESM::MaxStringRefIdFormatVersion, ESM::MaxUseEsmCellIdFormatVersion, }); for (ESM::FormatVersion v = result.back() + 1; v <= ESM::CurrentSaveGameFormatVersion; ++v) result.push_back(v); return result; } template > struct HasBlankFunction : std::false_type { }; template struct HasBlankFunction().blank())>> : std::true_type { }; template constexpr bool hasBlankFunction = HasBlankFunction::value; } /// Tests deletion of records. TYPED_TEST_P(StoreTest, delete_test) { using RecordType = TypeParam; for (const ESM::FormatVersion formatVersion : getFormats()) { SCOPED_TRACE("FormatVersion: " + std::to_string(formatVersion)); const ESM::RefId recordId = ESM::RefId::stringRefId("foobar"); RecordType record; if constexpr (hasBlankFunction) record.blank(); record.mId = recordId; ESM::ESMReader reader; ESM::Dialogue* dialogue = nullptr; { MWWorld::ESMStore esmStore; reader.open(getEsmFile(record, false, formatVersion), "filename"); esmStore.load(reader, &dummyListener, dialogue); // master file inserts a record esmStore.setUp(); EXPECT_EQ(esmStore.get().getSize(), 1); } { MWWorld::ESMStore esmStore; reader.open(getEsmFile(record, false, formatVersion), "filename"); esmStore.load(reader, &dummyListener, dialogue); // master file inserts a record reader.open(getEsmFile(record, true, formatVersion), "filename"); esmStore.load(reader, &dummyListener, dialogue); // now a plugin deletes it esmStore.setUp(); EXPECT_EQ(esmStore.get().getSize(), 0); } { MWWorld::ESMStore esmStore; reader.open(getEsmFile(record, false, formatVersion), "filename"); esmStore.load(reader, &dummyListener, dialogue); // master file inserts a record reader.open(getEsmFile(record, true, formatVersion), "filename"); esmStore.load(reader, &dummyListener, dialogue); // now a plugin deletes it // now another plugin inserts it again // expected behaviour is the record to reappear rather than staying deleted reader.open(getEsmFile(record, false, formatVersion), "filename"); esmStore.load(reader, &dummyListener, dialogue); esmStore.setUp(); EXPECT_EQ(esmStore.get().getSize(), 1); } } } template static unsigned int hasSameRecordId(const MWWorld::Store& store, ESM::RecNameInts RecName) { if constexpr (MWWorld::HasRecordId::value) { return T::sRecordId == RecName ? 1 : 0; } else { return 0; } } template static void testRecNameIntCount(const MWWorld::Store& store, const MWWorld::ESMStore::StoreTuple& stores) { if constexpr (MWWorld::HasRecordId::value) { const unsigned int recordIdCount = std::apply([](auto&&... x) { return (hasSameRecordId(x, T::sRecordId) + ...); }, stores); ASSERT_EQ(recordIdCount, static_cast(1)) << "The same RecNameInt is used twice ESM::REC_" << ESM::getRecNameString(T::sRecordId).toStringView(); } } static void testAllRecNameIntUnique(const MWWorld::ESMStore::StoreTuple& stores) { std::apply([&stores](auto&&... x) { (testRecNameIntCount(x, stores), ...); }, stores); } TEST(StoreTest, eachRecordTypeShouldHaveUniqueRecordId) { testAllRecNameIntUnique(MWWorld::ESMStore::StoreTuple()); } /// Tests overwriting of records. TYPED_TEST_P(StoreTest, overwrite_test) { using RecordType = TypeParam; for (const ESM::FormatVersion formatVersion : getFormats()) { SCOPED_TRACE("FormatVersion: " + std::to_string(formatVersion)); const ESM::RefId recordId = ESM::RefId::stringRefId("foobar"); const ESM::RefId recordIdUpper = ESM::RefId::stringRefId("Foobar"); RecordType record; if constexpr (hasBlankFunction) record.blank(); record.mId = recordId; ESM::ESMReader reader; ESM::Dialogue* dialogue = nullptr; MWWorld::ESMStore esmStore; // master file inserts a record reader.open(getEsmFile(record, false, formatVersion), "filename"); esmStore.load(reader, &dummyListener, dialogue); // now a plugin overwrites it with changed data record.mId = recordIdUpper; // change id to uppercase, to test case smashing while we're at it record.mModel = "the_new_model"; reader.open(getEsmFile(record, false, formatVersion), "filename"); esmStore.load(reader, &dummyListener, dialogue); esmStore.setUp(); // verify that changes were actually applied const RecordType* overwrittenRec = esmStore.get().search(recordId); ASSERT_NE(overwrittenRec, nullptr); EXPECT_EQ(overwrittenRec->mModel, "the_new_model"); } } namespace { using namespace ::testing; template struct StoreSaveLoadTest : public Test { }; TYPED_TEST_SUITE_P(StoreSaveLoadTest); TYPED_TEST_P(StoreSaveLoadTest, shouldNotChangeRefId) { using RecordType = TypeParam; const int index = 3; const std::string stringId = "foobar"; decltype(RecordType::mId) refId; if constexpr (ESM::hasIndex && !std::is_same_v) refId = RecordType::indexToRefId(index); else if constexpr (std::is_same_v) { refId = ESM::RefId::esm3ExteriorCell(0, 0); } else if constexpr (std::is_same_v) refId = ESM::Attribute::Strength; else if constexpr (std::is_same_v) refId = ESM::Skill::Block; else refId = ESM::StringRefId(stringId); for (const ESM::FormatVersion formatVersion : getFormats()) { SCOPED_TRACE("FormatVersion: " + std::to_string(formatVersion)); RecordType record; if constexpr (hasBlankFunction) record.blank(); record.mId = refId; if constexpr (ESM::hasStringId) record.mStringId = stringId; if constexpr (ESM::hasIndex) record.mIndex = index; if constexpr (std::is_same_v) record.mValue = ESM::Variant(42); ESM::ESMReader reader; ESM::Dialogue* dialogue = nullptr; MWWorld::ESMStore esmStore; if constexpr (std::is_same_v) { ASSERT_ANY_THROW(getEsmFile(record, false, formatVersion)); continue; } reader.open(getEsmFile(record, false, formatVersion), "filename"); ASSERT_NO_THROW(esmStore.load(reader, &dummyListener, dialogue)); esmStore.setUp(); const RecordType* result = nullptr; if constexpr (std::is_same_v) { const std::string* texture = esmStore.get().search(index, 0); ASSERT_NE(texture, nullptr); return; } else if constexpr (ESM::hasIndex) result = esmStore.get().search(index); else result = esmStore.get().search(refId); ASSERT_NE(result, nullptr); EXPECT_EQ(result->mId, refId); } } static_assert(ESM::hasIndex); static_assert(ESM::hasStringId); template > struct HasSaveFunction : std::false_type { }; template struct HasSaveFunction().save(std::declval(), bool()))>> : std::true_type { }; template struct ConcatTypes; template struct ConcatTypes> { using Type = std::tuple; }; template