1 #ifdef CATA_CATCH_PCH
2 #undef TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED
3 #define CATCH_CONFIG_IMPL_ONLY
4 #endif
5 #define CATCH_CONFIG_RUNNER
6 #include <algorithm>
7 #include <chrono>
8 #include <cstdio>
9 #include <cstdlib>
10 #include <cstring>
11 #include <ctime>
12 #include <exception>
13 #include <memory>
14 #include <ostream>
15 #include <string>
16 #include <utility>
17 #include <vector>
18 
19 #include "calendar.h"
20 #include "catch/catch.hpp"
21 #include "coordinates.h"
22 #ifndef _WIN32
23 #include <unistd.h>
24 #endif
25 
26 #include "avatar.h"
27 #include "cached_options.h"
28 #include "cata_assert.h"
29 #include "cata_utility.h"
30 #include "color.h"
31 #include "debug.h"
32 #include "filesystem.h"
33 #include "game.h"
34 #include "help.h"
35 #include "loading_ui.h"
36 #include "map.h"
37 #include "messages.h"
38 #include "options.h"
39 #include "output.h"
40 #include "overmap.h"
41 #include "overmapbuffer.h"
42 #include "path_info.h"
43 #include "pldata.h"
44 #include "rng.h"
45 #include "type_id.h"
46 #include "weather.h"
47 #include "worldfactory.h"
48 
49 using name_value_pair_t = std::pair<std::string, std::string>;
50 using option_overrides_t = std::vector<name_value_pair_t>;
51 
52 // If tag is found as a prefix of any argument in arg_vec, the argument is
53 // removed from arg_vec and the argument suffix after tag is returned.
54 // Otherwise, an empty string is returned and arg_vec is unchanged.
extract_argument(std::vector<const char * > & arg_vec,const std::string & tag)55 static std::string extract_argument( std::vector<const char *> &arg_vec, const std::string &tag )
56 {
57     std::string arg_rest;
58     for( auto iter = arg_vec.begin(); iter != arg_vec.end(); iter++ ) {
59         if( strncmp( *iter, tag.c_str(), tag.length() ) == 0 ) {
60             arg_rest = std::string( &( *iter )[tag.length()] );
61             arg_vec.erase( iter );
62             break;
63         }
64     }
65     return arg_rest;
66 }
67 
extract_mod_selection(std::vector<const char * > & arg_vec)68 static std::vector<mod_id> extract_mod_selection( std::vector<const char *> &arg_vec )
69 {
70     std::string mod_string = extract_argument( arg_vec, "--mods=" );
71 
72     std::vector<std::string> mod_names = string_split( mod_string, ',' );
73     std::vector<mod_id> ret;
74     for( const std::string &mod_name : mod_names ) {
75         if( !mod_name.empty() ) {
76             ret.emplace_back( mod_name );
77         }
78     }
79     // Always load test data mod
80     ret.emplace_back( "test_data" );
81 
82     return ret;
83 }
84 
init_global_game_state(const std::vector<mod_id> & mods,option_overrides_t & option_overrides,const std::string & user_dir)85 static void init_global_game_state( const std::vector<mod_id> &mods,
86                                     option_overrides_t &option_overrides,
87                                     const std::string &user_dir )
88 {
89     if( !assure_dir_exist( user_dir ) ) {
90         // NOLINTNEXTLINE(misc-static-assert,cert-dcl03-c)
91         cata_assert( !"Unable to make user_dir directory.  Check permissions." );
92     }
93 
94     PATH_INFO::init_base_path( "" );
95     PATH_INFO::init_user_dir( user_dir );
96     PATH_INFO::set_standard_filenames();
97 
98     if( !assure_dir_exist( PATH_INFO::config_dir() ) ) {
99         // NOLINTNEXTLINE(misc-static-assert,cert-dcl03-c)
100         cata_assert( !"Unable to make config directory.  Check permissions." );
101     }
102 
103     if( !assure_dir_exist( PATH_INFO::savedir() ) ) {
104         // NOLINTNEXTLINE(misc-static-assert,cert-dcl03-c)
105         cata_assert( !"Unable to make save directory.  Check permissions." );
106     }
107 
108     if( !assure_dir_exist( PATH_INFO::templatedir() ) ) {
109         // NOLINTNEXTLINE(misc-static-assert,cert-dcl03-c)
110         cata_assert( !"Unable to make templates directory.  Check permissions." );
111     }
112 
113     get_options().init();
114     get_options().load();
115 
116     // Apply command-line option overrides for test suite execution.
117     if( !option_overrides.empty() ) {
118         for( const name_value_pair_t &option : option_overrides ) {
119             if( get_options().has_option( option.first ) ) {
120                 options_manager::cOpt &opt = get_options().get_option( option.first );
121                 opt.setValue( option.second );
122             }
123         }
124     }
125     init_colors();
126 
127     g = std::make_unique<game>( );
128     g->new_game = true;
129     g->load_static_data();
130 
131     get_help().load();
132 
133     world_generator->set_active_world( nullptr );
134     world_generator->init();
135 #ifndef _WIN32
136     const std::string test_world_name = "Test World " + std::to_string( getpid() );
137 #else
138     const std::string test_world_name = "Test World";
139 #endif
140     WORLDPTR test_world = world_generator->make_new_world( test_world_name, mods );
141     cata_assert( test_world != nullptr );
142     world_generator->set_active_world( test_world );
143     cata_assert( world_generator->active_world != nullptr );
144 
145     calendar::set_eternal_season( get_option<bool>( "ETERNAL_SEASON" ) );
146     calendar::set_season_length( get_option<int>( "SEASON_LENGTH" ) );
147 
148     loading_ui ui( false );
149     g->load_core_data( ui );
150     g->load_world_modfiles( ui );
151 
152     get_avatar() = avatar();
153     get_avatar().create( character_type::NOW );
154     get_avatar().setID( g->assign_npc_id(), false );
155 
156     get_map() = map();
157 
158     overmap_special_batch empty_specials( point_abs_om{} );
159     overmap_buffer.create_custom_overmap( point_abs_om{}, empty_specials );
160 
161     map &here = get_map();
162     // TODO: fix point types
163     here.load( tripoint_abs_sm( here.get_abs_sub() ), false );
164 
165     get_weather().update_weather();
166 }
167 
168 // Checks if any of the flags are in container, removes them all
check_remove_flags(std::vector<const char * > & cont,const std::vector<const char * > & flags)169 static bool check_remove_flags( std::vector<const char *> &cont,
170                                 const std::vector<const char *> &flags )
171 {
172     bool has_any = false;
173     auto iter = flags.begin();
174     while( iter != flags.end() ) {
175         auto found = std::find_if( cont.begin(), cont.end(),
176         [iter]( const char *c ) {
177             return strcmp( c, *iter ) == 0;
178         } );
179         if( found == cont.end() ) {
180             iter++;
181         } else {
182             cont.erase( found );
183             has_any = true;
184         }
185     }
186 
187     return has_any;
188 }
189 
190 // Split s on separator sep, returning parts as a pair. Returns empty string as
191 // second value if no separator found.
split_pair(const std::string & s,const char sep)192 static name_value_pair_t split_pair( const std::string &s, const char sep )
193 {
194     const size_t pos = s.find( sep );
195     if( pos != std::string::npos ) {
196         return name_value_pair_t( s.substr( 0, pos ), s.substr( pos + 1 ) );
197     } else {
198         return name_value_pair_t( s, "" );
199     }
200 }
201 
extract_option_overrides(std::vector<const char * > & arg_vec)202 static option_overrides_t extract_option_overrides( std::vector<const char *> &arg_vec )
203 {
204     option_overrides_t ret;
205     std::string option_overrides_string = extract_argument( arg_vec, "--option_overrides=" );
206     if( option_overrides_string.empty() ) {
207         return ret;
208     }
209     const char delim = ',';
210     const char sep = ':';
211     size_t i = 0;
212     size_t pos = option_overrides_string.find( delim );
213     while( pos != std::string::npos ) {
214         std::string part = option_overrides_string.substr( i, pos );
215         ret.emplace_back( split_pair( part, sep ) );
216         i = ++pos;
217         pos = option_overrides_string.find( delim, pos );
218     }
219     // Handle last part
220     const std::string part = option_overrides_string.substr( i );
221     ret.emplace_back( split_pair( part, sep ) );
222     return ret;
223 }
224 
extract_user_dir(std::vector<const char * > & arg_vec)225 static std::string extract_user_dir( std::vector<const char *> &arg_vec )
226 {
227     std::string option_user_dir = extract_argument( arg_vec, "--user-dir=" );
228     if( option_user_dir.empty() ) {
229         return "./test_user_dir/";
230     }
231     if( !string_ends_with( option_user_dir, "/" ) ) {
232         option_user_dir += "/";
233     }
234     return option_user_dir;
235 }
236 
237 struct CataListener : Catch::TestEventListenerBase {
238     using TestEventListenerBase::TestEventListenerBase;
239 
sectionStartingCataListener240     void sectionStarting( Catch::SectionInfo const &sectionInfo ) override {
241         TestEventListenerBase::sectionStarting( sectionInfo );
242         // Initialize the cata RNG with the Catch seed for reproducible tests
243         rng_set_engine_seed( m_config->rngSeed() );
244         // Clear the message log so on test failures we see only messages from
245         // during that test
246         Messages::clear_messages();
247     }
248 
sectionEndedCataListener249     void sectionEnded( Catch::SectionStats const &sectionStats ) override {
250         TestEventListenerBase::sectionEnded( sectionStats );
251         if( !sectionStats.assertions.allPassed() ) {
252             std::vector<std::pair<std::string, std::string>> messages =
253                         Messages::recent_messages( 0 );
254             if( !messages.empty() ) {
255                 stream << "Log messages during failed test:\n";
256             }
257             for( const std::pair<std::string, std::string> &message : messages ) {
258                 stream << message.first << ": " << message.second << '\n';
259             }
260             Messages::clear_messages();
261         }
262     }
263 
assertionEndedCataListener264     bool assertionEnded( Catch::AssertionStats const &assertionStats ) override {
265 #ifdef BACKTRACE
266         Catch::AssertionResult const &result = assertionStats.assertionResult;
267 
268         if( result.getResultType() == Catch::ResultWas::FatalErrorCondition ) {
269             // We are in a signal handler for a fatal error condition, so print a
270             // backtrace
271             stream << "Stack trace at fatal error:\n";
272             debug_write_backtrace( stream );
273         }
274 #endif
275 
276         return TestEventListenerBase::assertionEnded( assertionStats );
277     }
278 };
279 
CATCH_REGISTER_LISTENER(CataListener)280 CATCH_REGISTER_LISTENER( CataListener )
281 
282 int main( int argc, const char *argv[] )
283 {
284     Catch::Session session;
285 
286     std::vector<const char *> arg_vec( argv, argv + argc );
287 
288     std::vector<mod_id> mods = extract_mod_selection( arg_vec );
289     if( std::find( mods.begin(), mods.end(), mod_id( "dda" ) ) == mods.end() ) {
290         mods.insert( mods.begin(), mod_id( "dda" ) ); // @todo move unit test items to core
291     }
292 
293     option_overrides_t option_overrides_for_test_suite = extract_option_overrides( arg_vec );
294 
295     const bool dont_save = check_remove_flags( arg_vec, { "-D", "--drop-world" } );
296 
297     std::string user_dir = extract_user_dir( arg_vec );
298 
299     std::string error_fmt = extract_argument( arg_vec, "--error-format=" );
300     if( error_fmt == "github-action" ) {
301         // NOLINTNEXTLINE(cata-tests-must-restore-global-state)
302         error_log_format = error_log_format_t::github_action;
303     } else if( error_fmt == "human-readable" || error_fmt.empty() ) {
304         // NOLINTNEXTLINE(cata-tests-must-restore-global-state)
305         error_log_format = error_log_format_t::human_readable;
306     } else {
307         printf( "Unknown format %s", error_fmt.c_str() );
308         return EXIT_FAILURE;
309     }
310 
311     // Note: this must not be invoked before all DDA-specific flags are stripped from arg_vec!
312     int result = session.applyCommandLine( arg_vec.size(), &arg_vec[0] );
313     if( result != 0 || session.configData().showHelp ) {
314         printf( "CataclysmDDA specific options:\n" );
315         printf( "  --mods=<mod1,mod2,…>         Loads the list of mods before executing tests.\n" );
316         printf( "  --user-dir=<dir>             Set user dir (where test world will be created).\n" );
317         printf( "  -D, --drop-world             Don't save the world on test failure.\n" );
318         printf( "  --option_overrides=n:v[,…]   Name-value pairs of game options for tests.\n" );
319         printf( "                               (overrides config/options.json values)\n" );
320         printf( "  --error-format=<value>       Format of error messages.  Possible values are:\n" );
321         printf( "                                   human-readable (default)\n" );
322         printf( "                                   github-action\n" );
323         return result;
324     }
325 
326     // NOLINTNEXTLINE(cata-tests-must-restore-global-state)
327     test_mode = true;
328 
329     on_out_of_scope print_newline( []() {
330         printf( "\n" );
331     } );
332 
333     setupDebug( DebugOutput::std_err );
334 
335     // Set the seed for mapgen (the seed will also be reset before each test)
336     const unsigned int seed = session.config().rngSeed();
337     if( seed ) {
338         rng_set_engine_seed( seed );
339 
340         // If the run is terminated due to a crash during initialization, we won't
341         // see the seed unless it's printed out in advance, so do that here.
342         DebugLog( D_INFO, DC_ALL ) << "Randomness seeded to: " << seed;
343     }
344 
345     try {
346         // TODO: Only init game if we're running tests that need it.
347         init_global_game_state( mods, option_overrides_for_test_suite, user_dir );
348     } catch( const std::exception &err ) {
349         DebugLog( D_ERROR, DC_ALL ) << "Terminated:\n" << err.what();
350         DebugLog( D_INFO, DC_ALL ) <<
351                                    "Make sure that you're in the correct working directory and your data isn't corrupted.";
352         return EXIT_FAILURE;
353     }
354 
355     bool error_during_initialization = debug_has_error_been_observed();
356 
357     const auto start = std::chrono::system_clock::now();
358     std::time_t start_time = std::chrono::system_clock::to_time_t( start );
359     // Leading newline in case there were debug messages during
360     // initialization.
361     DebugLog( D_INFO, DC_ALL ) << "Starting the actual test at " << std::ctime( &start_time );
362     result = session.run();
363     const auto end = std::chrono::system_clock::now();
364     std::time_t end_time = std::chrono::system_clock::to_time_t( end );
365 
366     auto world_name = world_generator->active_world->world_name;
367     if( result == 0 || dont_save ) {
368         world_generator->delete_world( world_name, true );
369     } else {
370         DebugLog( D_INFO, DC_ALL ) << "Test world " << world_name << " left for inspection.";
371     }
372 
373     std::chrono::duration<double> elapsed_seconds = end - start;
374     DebugLog( D_INFO, DC_ALL ) << "Ended test at " << std::ctime( &end_time );
375     DebugLog( D_INFO, DC_ALL ) << "The test took " << elapsed_seconds.count() << " seconds";
376 
377     if( seed ) {
378         // Also print the seed at the end so it can be easily found
379         DebugLog( D_INFO, DC_ALL ) << "Randomness seeded to: " << seed;
380     }
381 
382     if( error_during_initialization ) {
383         DebugLog( D_INFO, DC_ALL ) <<
384                                    "Treating result as failure due to error logged during initialization.";
385         return 1;
386     }
387 
388     if( debug_has_error_been_observed() ) {
389         DebugLog( D_INFO, DC_ALL ) << "Treating result as failure due to error logged during tests.";
390         return 1;
391     }
392 
393     return result;
394 }
395