|
|
|
@ -1,5 +1,6 @@
|
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
|
|
|
|
|
#include <boost/program_options/options_description.hpp>
|
|
|
|
@ -16,6 +17,7 @@
|
|
|
|
|
#include <components/esm4/loadstat.hpp>
|
|
|
|
|
#include <components/esm4/reader.hpp>
|
|
|
|
|
#include <components/esm4/readerutils.hpp>
|
|
|
|
|
#include <components/esm4/typetraits.hpp>
|
|
|
|
|
#include <components/files/configurationmanager.hpp>
|
|
|
|
|
#include <components/files/conversion.hpp>
|
|
|
|
|
#include <components/loadinglistener/loadinglistener.hpp>
|
|
|
|
@ -238,20 +240,21 @@ TEST_F(ContentFileTest, autocalc_test)
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/// Base class for tests of ESMStore that do not rely on external content files
|
|
|
|
|
template <class T>
|
|
|
|
|
struct StoreTest : public ::testing::Test
|
|
|
|
|
{
|
|
|
|
|
protected:
|
|
|
|
|
MWWorld::ESMStore mEsmStore;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
TYPED_TEST_SUITE_P(StoreTest);
|
|
|
|
|
|
|
|
|
|
/// Create an ESM file in-memory containing the specified record.
|
|
|
|
|
/// @param deleted Write record with deleted flag?
|
|
|
|
|
template <typename T>
|
|
|
|
|
std::unique_ptr<std::istream> getEsmFile(T record, bool deleted)
|
|
|
|
|
std::unique_ptr<std::istream> getEsmFile(T record, bool deleted, ESM::FormatVersion formatVersion)
|
|
|
|
|
{
|
|
|
|
|
ESM::ESMWriter writer;
|
|
|
|
|
auto stream = std::make_unique<std::stringstream>();
|
|
|
|
|
writer.setFormat(0);
|
|
|
|
|
writer.setFormatVersion(formatVersion);
|
|
|
|
|
writer.save(*stream);
|
|
|
|
|
writer.startRecord(T::sRecordId);
|
|
|
|
|
record.save(writer, deleted);
|
|
|
|
@ -260,41 +263,79 @@ std::unique_ptr<std::istream> getEsmFile(T record, bool deleted)
|
|
|
|
|
return stream;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
constexpr std::array formats = {
|
|
|
|
|
ESM::DefaultFormatVersion,
|
|
|
|
|
ESM::CurrentContentFormatVersion,
|
|
|
|
|
ESM::MaxOldWeatherFormatVersion,
|
|
|
|
|
ESM::MaxOldDeathAnimationFormatVersion,
|
|
|
|
|
ESM::MaxOldForOfWarFormatVersion,
|
|
|
|
|
ESM::MaxWerewolfDeprecatedDataFormatVersion,
|
|
|
|
|
ESM::MaxOldTimeLeftFormatVersion,
|
|
|
|
|
ESM::MaxIntFallbackFormatVersion,
|
|
|
|
|
ESM::MaxClearModifiersFormatVersion,
|
|
|
|
|
ESM::MaxOldAiPackageFormatVersion,
|
|
|
|
|
ESM::MaxOldSkillsAndAttributesFormatVersion,
|
|
|
|
|
ESM::MaxOldCreatureStatsFormatVersion,
|
|
|
|
|
ESM::CurrentSaveGameFormatVersion,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template <class T, class = std::void_t<>>
|
|
|
|
|
struct HasBlankFunction : std::false_type
|
|
|
|
|
{
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template <class T>
|
|
|
|
|
struct HasBlankFunction<T, std::void_t<decltype(std::declval<T>().blank())>> : std::true_type
|
|
|
|
|
{
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template <class T>
|
|
|
|
|
constexpr bool hasBlankFunction = HasBlankFunction<T>::value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Tests deletion of records.
|
|
|
|
|
TEST_F(StoreTest, delete_test)
|
|
|
|
|
TYPED_TEST_P(StoreTest, delete_test)
|
|
|
|
|
{
|
|
|
|
|
const ESM::RefId recordId = ESM::RefId::stringRefId("foobar");
|
|
|
|
|
using RecordType = TypeParam;
|
|
|
|
|
|
|
|
|
|
typedef ESM::Apparatus RecordType;
|
|
|
|
|
for (const ESM::FormatVersion formatVersion : formats)
|
|
|
|
|
{
|
|
|
|
|
SCOPED_TRACE("FormatVersion: " + std::to_string(formatVersion));
|
|
|
|
|
const ESM::RefId recordId = ESM::RefId::stringRefId("foobar");
|
|
|
|
|
|
|
|
|
|
RecordType record;
|
|
|
|
|
record.blank();
|
|
|
|
|
record.mId = recordId;
|
|
|
|
|
RecordType record;
|
|
|
|
|
if constexpr (hasBlankFunction<RecordType>)
|
|
|
|
|
record.blank();
|
|
|
|
|
record.mId = recordId;
|
|
|
|
|
|
|
|
|
|
ESM::ESMReader reader;
|
|
|
|
|
ESM::Dialogue* dialogue = nullptr;
|
|
|
|
|
ESM::ESMReader reader;
|
|
|
|
|
ESM::Dialogue* dialogue = nullptr;
|
|
|
|
|
MWWorld::ESMStore esmStore;
|
|
|
|
|
|
|
|
|
|
// master file inserts a record
|
|
|
|
|
reader.open(getEsmFile(record, false), "filename");
|
|
|
|
|
mEsmStore.load(reader, &dummyListener, dialogue);
|
|
|
|
|
mEsmStore.setUp();
|
|
|
|
|
// master file inserts a record
|
|
|
|
|
reader.open(getEsmFile(record, false, formatVersion), "filename");
|
|
|
|
|
esmStore.load(reader, &dummyListener, dialogue);
|
|
|
|
|
esmStore.setUp();
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(mEsmStore.get<RecordType>().getSize() == 1);
|
|
|
|
|
EXPECT_EQ(esmStore.get<RecordType>().getSize(), 1);
|
|
|
|
|
|
|
|
|
|
// now a plugin deletes it
|
|
|
|
|
reader.open(getEsmFile(record, true), "filename");
|
|
|
|
|
mEsmStore.load(reader, &dummyListener, dialogue);
|
|
|
|
|
mEsmStore.setUp();
|
|
|
|
|
// now a plugin deletes it
|
|
|
|
|
reader.open(getEsmFile(record, true, formatVersion), "filename");
|
|
|
|
|
esmStore.load(reader, &dummyListener, dialogue);
|
|
|
|
|
esmStore.setUp();
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(mEsmStore.get<RecordType>().getSize() == 0);
|
|
|
|
|
EXPECT_EQ(esmStore.get<RecordType>().getSize(), 0);
|
|
|
|
|
|
|
|
|
|
// now another plugin inserts it again
|
|
|
|
|
// expected behaviour is the record to reappear rather than staying deleted
|
|
|
|
|
reader.open(getEsmFile(record, false), "filename");
|
|
|
|
|
mEsmStore.load(reader, &dummyListener, dialogue);
|
|
|
|
|
mEsmStore.setUp();
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(mEsmStore.get<RecordType>().getSize() == 1);
|
|
|
|
|
EXPECT_EQ(esmStore.get<RecordType>().getSize(), 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
@ -327,42 +368,204 @@ static void testAllRecNameIntUnique(const MWWorld::ESMStore::StoreTuple& stores)
|
|
|
|
|
std::apply([&stores](auto&&... x) { (testRecNameIntCount(x, stores), ...); }, stores);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_F(StoreTest, eachRecordTypeShouldHaveUniqueRecordId)
|
|
|
|
|
TEST(StoreTest, eachRecordTypeShouldHaveUniqueRecordId)
|
|
|
|
|
{
|
|
|
|
|
testAllRecNameIntUnique(MWWorld::ESMStore::StoreTuple());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Tests overwriting of records.
|
|
|
|
|
TEST_F(StoreTest, overwrite_test)
|
|
|
|
|
TYPED_TEST_P(StoreTest, overwrite_test)
|
|
|
|
|
{
|
|
|
|
|
using RecordType = TypeParam;
|
|
|
|
|
|
|
|
|
|
for (const ESM::FormatVersion formatVersion : formats)
|
|
|
|
|
{
|
|
|
|
|
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<RecordType>)
|
|
|
|
|
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);
|
|
|
|
|
esmStore.setUp();
|
|
|
|
|
|
|
|
|
|
// 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<RecordType>().search(recordId);
|
|
|
|
|
|
|
|
|
|
ASSERT_NE(overwrittenRec, nullptr);
|
|
|
|
|
|
|
|
|
|
EXPECT_EQ(overwrittenRec->mModel, "the_new_model");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
const ESM::RefId recordId = ESM::RefId::stringRefId("foobar");
|
|
|
|
|
const ESM::RefId recordIdUpper = ESM::RefId::stringRefId("Foobar");
|
|
|
|
|
template <class T>
|
|
|
|
|
struct StoreSaveLoadTest : public ::testing::Test
|
|
|
|
|
{
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template <class T, class = std::void_t<>>
|
|
|
|
|
struct HasIndex : std::false_type
|
|
|
|
|
{
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template <class T>
|
|
|
|
|
struct HasIndex<T, std::void_t<decltype(T::mIndex)>> : std::true_type
|
|
|
|
|
{
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template <class T>
|
|
|
|
|
constexpr bool hasIndex = HasIndex<T>::value;
|
|
|
|
|
|
|
|
|
|
TYPED_TEST_SUITE_P(StoreSaveLoadTest);
|
|
|
|
|
|
|
|
|
|
TYPED_TEST_P(StoreSaveLoadTest, shouldNotChangeRefId)
|
|
|
|
|
{
|
|
|
|
|
using RecordType = TypeParam;
|
|
|
|
|
|
|
|
|
|
const int index = 3;
|
|
|
|
|
ESM::RefId refId;
|
|
|
|
|
if constexpr (hasIndex<RecordType> && !std::is_same_v<RecordType, ESM::LandTexture>)
|
|
|
|
|
refId = ESM::RefId::stringRefId(RecordType::indexToId(index));
|
|
|
|
|
else
|
|
|
|
|
refId = ESM::RefId::stringRefId("foobar");
|
|
|
|
|
|
|
|
|
|
for (const ESM::FormatVersion formatVersion : formats)
|
|
|
|
|
{
|
|
|
|
|
SCOPED_TRACE("FormatVersion: " + std::to_string(formatVersion));
|
|
|
|
|
|
|
|
|
|
RecordType record;
|
|
|
|
|
|
|
|
|
|
if constexpr (hasBlankFunction<RecordType>)
|
|
|
|
|
record.blank();
|
|
|
|
|
|
|
|
|
|
record.mId = refId;
|
|
|
|
|
|
|
|
|
|
if constexpr (hasIndex<RecordType>)
|
|
|
|
|
record.mIndex = index;
|
|
|
|
|
|
|
|
|
|
if constexpr (std::is_same_v<RecordType, ESM::Global>)
|
|
|
|
|
record.mValue = ESM::Variant(42);
|
|
|
|
|
|
|
|
|
|
ESM::ESMReader reader;
|
|
|
|
|
ESM::Dialogue* dialogue = nullptr;
|
|
|
|
|
MWWorld::ESMStore esmStore;
|
|
|
|
|
|
|
|
|
|
reader.open(getEsmFile(record, false, formatVersion), "filename");
|
|
|
|
|
esmStore.load(reader, &dummyListener, dialogue);
|
|
|
|
|
esmStore.setUp();
|
|
|
|
|
|
|
|
|
|
const RecordType* result = nullptr;
|
|
|
|
|
if constexpr (std::is_same_v<RecordType, ESM::LandTexture>)
|
|
|
|
|
result = esmStore.get<RecordType>().search(index, 0);
|
|
|
|
|
else if constexpr (hasIndex<RecordType>)
|
|
|
|
|
result = esmStore.get<RecordType>().search(index);
|
|
|
|
|
else
|
|
|
|
|
result = esmStore.get<RecordType>().search(refId);
|
|
|
|
|
|
|
|
|
|
ASSERT_NE(result, nullptr);
|
|
|
|
|
EXPECT_EQ(result->mId, refId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static_assert(hasIndex<ESM::MagicEffect>);
|
|
|
|
|
|
|
|
|
|
template <class T, class = std::void_t<>>
|
|
|
|
|
struct HasSaveFunction : std::false_type
|
|
|
|
|
{
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
typedef ESM::Apparatus RecordType;
|
|
|
|
|
template <class T>
|
|
|
|
|
struct HasSaveFunction<T, std::void_t<decltype(std::declval<T>().save(std::declval<ESM::ESMWriter&>(), bool()))>>
|
|
|
|
|
: std::true_type
|
|
|
|
|
{
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template <class Head, class List>
|
|
|
|
|
struct ConcatTypes;
|
|
|
|
|
|
|
|
|
|
template <class Head, class... Ts>
|
|
|
|
|
struct ConcatTypes<Head, std::tuple<Ts...>>
|
|
|
|
|
{
|
|
|
|
|
using Type = std::tuple<Head, Ts...>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template <template <class...> class Predicate, class Out, class... Ins>
|
|
|
|
|
struct FilterTypesImpl;
|
|
|
|
|
|
|
|
|
|
template <template <class...> class Predicate, class Out, class Head, class... Tail>
|
|
|
|
|
struct FilterTypesImpl<Predicate, Out, Head, Tail...>
|
|
|
|
|
{
|
|
|
|
|
using Type = typename FilterTypesImpl<Predicate,
|
|
|
|
|
std::conditional_t<Predicate<Head>::value, typename ConcatTypes<Head, Out>::Type, Out>, Tail...>::Type;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template <template <class...> class Predicate, class Out>
|
|
|
|
|
struct FilterTypesImpl<Predicate, Out>
|
|
|
|
|
{
|
|
|
|
|
using Type = Out;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
RecordType record;
|
|
|
|
|
record.blank();
|
|
|
|
|
record.mId = recordId;
|
|
|
|
|
template <template <class...> class Predicate, class List>
|
|
|
|
|
struct FilterTypes;
|
|
|
|
|
|
|
|
|
|
ESM::ESMReader reader;
|
|
|
|
|
ESM::Dialogue* dialogue = nullptr;
|
|
|
|
|
template <template <class...> class Predicate, class... Ts>
|
|
|
|
|
struct FilterTypes<Predicate, std::tuple<Ts...>>
|
|
|
|
|
{
|
|
|
|
|
using Type = typename FilterTypesImpl<Predicate, std::tuple<>, Ts...>::Type;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
template <class... T>
|
|
|
|
|
struct ToRecordTypes;
|
|
|
|
|
|
|
|
|
|
// master file inserts a record
|
|
|
|
|
reader.open(getEsmFile(record, false), "filename");
|
|
|
|
|
mEsmStore.load(reader, &dummyListener, dialogue);
|
|
|
|
|
mEsmStore.setUp();
|
|
|
|
|
template <class... T>
|
|
|
|
|
struct ToRecordTypes<std::tuple<MWWorld::Store<T>...>>
|
|
|
|
|
{
|
|
|
|
|
using Type = std::tuple<T...>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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), "filename");
|
|
|
|
|
mEsmStore.load(reader, &dummyListener, dialogue);
|
|
|
|
|
mEsmStore.setUp();
|
|
|
|
|
template <class... T>
|
|
|
|
|
struct AsTestingTypes;
|
|
|
|
|
|
|
|
|
|
template <class... T>
|
|
|
|
|
struct AsTestingTypes<std::tuple<T...>>
|
|
|
|
|
{
|
|
|
|
|
using Type = testing::Types<T...>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// verify that changes were actually applied
|
|
|
|
|
const RecordType* overwrittenRec = mEsmStore.get<RecordType>().search(recordId);
|
|
|
|
|
using RecordTypes = typename ToRecordTypes<MWWorld::ESMStore::StoreTuple>::Type;
|
|
|
|
|
using RecordTypesWithId = typename FilterTypes<ESM4::HasId, RecordTypes>::Type;
|
|
|
|
|
using RecordTypesWithSave = typename FilterTypes<HasSaveFunction, RecordTypesWithId>::Type;
|
|
|
|
|
using RecordTypesWithModel = typename FilterTypes<ESM4::HasModel, RecordTypesWithSave>::Type;
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(overwrittenRec != nullptr);
|
|
|
|
|
REGISTER_TYPED_TEST_SUITE_P(StoreSaveLoadTest, shouldNotChangeRefId);
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(overwrittenRec && overwrittenRec->mModel == "the_new_model");
|
|
|
|
|
static_assert(std::tuple_size_v<RecordTypesWithSave> == 38);
|
|
|
|
|
|
|
|
|
|
INSTANTIATE_TYPED_TEST_SUITE_P(
|
|
|
|
|
RecordTypesTest, StoreSaveLoadTest, typename AsTestingTypes<RecordTypesWithSave>::Type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
REGISTER_TYPED_TEST_SUITE_P(StoreTest, overwrite_test, delete_test);
|
|
|
|
|
|
|
|
|
|
static_assert(std::tuple_size_v<RecordTypesWithModel> == 19);
|
|
|
|
|
|
|
|
|
|
INSTANTIATE_TYPED_TEST_SUITE_P(RecordTypesTest, StoreTest, typename AsTestingTypes<RecordTypesWithModel>::Type);
|
|
|
|
|