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 §ionInfo ) 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 §ionStats ) 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