# include <gtest/gtest.h>
# include <boost/filesystem/fstream.hpp>
# include <components/files/configurationmanager.hpp>
# include <components/esm/esmreader.hpp>
# include <components/esm/esmwriter.hpp>
# include <components/loadinglistener/loadinglistener.hpp>
# 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 :
virtual void SetUp ( )
{
readContentFiles ( ) ;
// load the content files
std : : vector < ESM : : ESMReader > readerList ;
readerList . resize ( mContentFiles . size ( ) ) ;
int index = 0 ;
for ( std : : vector < boost : : filesystem : : path > : : const_iterator it = mContentFiles . begin ( ) ; it ! = mContentFiles . end ( ) ; + + it )
{
ESM : : ESMReader lEsm ;
lEsm . setEncoder ( NULL ) ;
lEsm . setIndex ( index ) ;
lEsm . setGlobalReaderList ( & readerList ) ;
lEsm . open ( it - > string ( ) ) ;
readerList [ index ] = lEsm ;
mEsmStore . load ( readerList [ index ] , & dummyListener ) ;
+ + index ;
}
mEsmStore . setUp ( ) ;
}
virtual void TearDown ( )
{
}
// 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 " ) ;
desc . add_options ( )
( " data " , boost : : program_options : : value < Files : : PathContainer > ( ) - > default_value ( Files : : PathContainer ( ) , " data " ) - > multitoken ( ) - > composing ( ) )
( " content " , boost : : program_options : : value < std : : vector < std : : string > > ( ) - > default_value ( std : : vector < std : : string > ( ) , " " )
- > multitoken ( ) , " content file(s): esm/esp, or omwgame/omwaddon " )
( " data-local " , boost : : program_options : : value < std : : string > ( ) - > default_value ( " " ) ) ;
boost : : program_options : : notify ( variables ) ;
mConfigurationManager . readConfiguration ( variables , desc , true ) ;
Files : : PathContainer dataDirs , dataLocal ;
if ( ! variables [ " data " ] . empty ( ) ) {
dataDirs = Files : : PathContainer ( variables [ " data " ] . as < Files : : PathContainer > ( ) ) ;
}
std : : string local = variables [ " data-local " ] . as < std : : string > ( ) ;
if ( ! local . empty ( ) ) {
dataLocal . push_back ( Files : : PathContainer : : value_type ( local ) ) ;
}
mConfigurationManager . processPaths ( dataDirs ) ;
mConfigurationManager . processPaths ( dataLocal , true ) ;
if ( ! dataLocal . empty ( ) )
dataDirs . insert ( dataDirs . end ( ) , dataLocal . begin ( ) , dataLocal . end ( ) ) ;
Files : : Collections collections ( dataDirs , true ) ;
std : : vector < std : : string > contentFiles = variables [ " content " ] . as < std : : vector < std : : string > > ( ) ;
for ( std : : vector < std : : string > : : iterator it = contentFiles . begin ( ) ; it ! = contentFiles . end ( ) ; + + it )
mContentFiles . push_back ( collections . getPath ( * it ) ) ;
}
protected :
Files : : ConfigurationManager mConfigurationManager ;
MWWorld : : ESMStore mEsmStore ;
std : : vector < boost : : filesystem : : path > 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 std : : string file = " test_dialogue_merging.txt " ;
boost : : filesystem : : ofstream stream ;
stream . open ( file ) ;
const MWWorld : : Store < ESM : : Dialogue > & dialStore = mEsmStore . get < ESM : : Dialogue > ( ) ;
for ( MWWorld : : Store < ESM : : Dialogue > : : iterator it = dialStore . begin ( ) ; it ! = dialStore . end ( ) ; + + it )
{
const ESM : : Dialogue & dial = * it ;
stream < < " Dialogue: " < < dial . mId < < std : : endl ;
for ( ESM : : Dialogue : : InfoContainer : : const_iterator infoIt = dial . mInfo . begin ( ) ; infoIt ! = dial . mInfo . end ( ) ; + + infoIt )
{
const ESM : : DialInfo & info = * infoIt ;
stream < < info . mId < < std : : endl ;
}
stream < < std : : endl ;
}
std : : cout < < " dialogue_merging_test successful, results printed to " < < 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 < ESM : : Activator > ( arg1 , arg2 ) ; \
func < ESM : : Apparatus > ( arg1 , arg2 ) ; \
func < ESM : : Armor > ( arg1 , arg2 ) ; \
func < ESM : : BirthSign > ( arg1 , arg2 ) ; \
func < ESM : : BodyPart > ( arg1 , arg2 ) ; \
func < ESM : : Book > ( arg1 , arg2 ) ; \
func < ESM : : Class > ( arg1 , arg2 ) ; \
func < ESM : : Clothing > ( arg1 , arg2 ) ; \
func < ESM : : Container > ( arg1 , arg2 ) ; \
func < ESM : : Creature > ( arg1 , arg2 ) ; \
func < ESM : : CreatureLevList > ( arg1 , arg2 ) ; \
func < ESM : : Dialogue > ( arg1 , arg2 ) ; \
func < ESM : : Door > ( arg1 , arg2 ) ; \
func < ESM : : Enchantment > ( arg1 , arg2 ) ; \
func < ESM : : Faction > ( arg1 , arg2 ) ; \
func < ESM : : GameSetting > ( arg1 , arg2 ) ; \
func < ESM : : Global > ( arg1 , arg2 ) ; \
func < ESM : : Ingredient > ( arg1 , arg2 ) ; \
func < ESM : : ItemLevList > ( arg1 , arg2 ) ; \
func < ESM : : Light > ( arg1 , arg2 ) ; \
func < ESM : : Lockpick > ( arg1 , arg2 ) ; \
func < ESM : : Miscellaneous > ( arg1 , arg2 ) ; \
func < ESM : : NPC > ( arg1 , arg2 ) ; \
func < ESM : : Potion > ( arg1 , arg2 ) ; \
func < ESM : : Probe > ( arg1 , arg2 ) ; \
func < ESM : : Race > ( arg1 , arg2 ) ; \
func < ESM : : Region > ( arg1 , arg2 ) ; \
func < ESM : : Repair > ( arg1 , arg2 ) ; \
func < ESM : : Script > ( arg1 , arg2 ) ; \
func < ESM : : Sound > ( arg1 , arg2 ) ; \
func < ESM : : SoundGenerator > ( arg1 , arg2 ) ; \
func < ESM : : Spell > ( arg1 , arg2 ) ; \
func < ESM : : StartScript > ( arg1 , arg2 ) ; \
func < ESM : : Weapon > ( arg1 , arg2 ) ;
template < typename T >
void printRecords ( MWWorld : : ESMStore & esmStore , std : : ostream & outStream )
{
const MWWorld : : Store < T > & store = esmStore . get < T > ( ) ;
outStream < < store . getSize ( ) < < " " < < T : : getRecordType ( ) < < " records " < < std : : endl ;
for ( typename MWWorld : : Store < T > : : 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 std : : string file = " test_content_diagnostics.txt " ;
boost : : filesystem : : ofstream stream ;
stream . open ( file ) ;
RUN_TEST_FOR_TYPES ( printRecords , mEsmStore , stream ) ;
std : : cout < < " diagnostics_test successful, results printed to " < < 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
struct StoreTest : public : : testing : : Test
{
protected :
MWWorld : : ESMStore mEsmStore ;
} ;
/// Create an ESM file in-memory containing the specified record.
/// @param deleted Write record with deleted flag?
template < typename T >
Files : : IStreamPtr getEsmFile ( T record , bool deleted )
{
ESM : : ESMWriter writer ;
std : : stringstream * stream = new std : : stringstream ;
writer . setFormat ( 0 ) ;
writer . save ( * stream ) ;
writer . startRecord ( T : : sRecordId ) ;
record . save ( writer , deleted ) ;
writer . endRecord ( T : : sRecordId ) ;
return Files : : IStreamPtr ( stream ) ;
}
/// Tests deletion of records.
TEST_F ( StoreTest , delete_test )
{
const std : : string recordId = " foobar " ;
typedef ESM : : Apparatus RecordType ;
RecordType record ;
record . blank ( ) ;
record . mId = recordId ;
ESM : : ESMReader reader ;
std : : vector < ESM : : ESMReader > readerList ;
readerList . push_back ( reader ) ;
reader . setGlobalReaderList ( & readerList ) ;
// master file inserts a record
Files : : IStreamPtr file = getEsmFile ( record , false ) ;
reader . open ( file , " filename " ) ;
mEsmStore . load ( reader , & dummyListener ) ;
mEsmStore . setUp ( ) ;
ASSERT_TRUE ( mEsmStore . get < RecordType > ( ) . getSize ( ) = = 1 ) ;
// now a plugin deletes it
file = getEsmFile ( record , true ) ;
reader . open ( file , " filename " ) ;
mEsmStore . load ( reader , & dummyListener ) ;
mEsmStore . setUp ( ) ;
ASSERT_TRUE ( mEsmStore . get < RecordType > ( ) . getSize ( ) = = 0 ) ;
// now another plugin inserts it again
// expected behaviour is the record to reappear rather than staying deleted
file = getEsmFile ( record , false ) ;
reader . open ( file , " filename " ) ;
mEsmStore . load ( reader , & dummyListener ) ;
mEsmStore . setUp ( ) ;
ASSERT_TRUE ( mEsmStore . get < RecordType > ( ) . getSize ( ) = = 1 ) ;
}
/// Tests overwriting of records.
TEST_F ( StoreTest , overwrite_test )
{
const std : : string recordId = " foobar " ;
const std : : string recordIdUpper = " Foobar " ;
typedef ESM : : Apparatus RecordType ;
RecordType record ;
record . blank ( ) ;
record . mId = recordId ;
ESM : : ESMReader reader ;
std : : vector < ESM : : ESMReader > readerList ;
readerList . push_back ( reader ) ;
reader . setGlobalReaderList ( & readerList ) ;
// master file inserts a record
Files : : IStreamPtr file = getEsmFile ( record , false ) ;
reader . open ( file , " filename " ) ;
mEsmStore . load ( reader , & dummyListener ) ;
mEsmStore . 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 " ;
file = getEsmFile ( record , false ) ;
reader . open ( file , " filename " ) ;
mEsmStore . load ( reader , & dummyListener ) ;
mEsmStore . setUp ( ) ;
// verify that changes were actually applied
const RecordType * overwrittenRec = mEsmStore . get < RecordType > ( ) . search ( recordId ) ;
ASSERT_TRUE ( overwrittenRec ! = NULL ) ;
ASSERT_TRUE ( overwrittenRec & & overwrittenRec - > mModel = = " the_new_model " ) ;
}