/** \file fish_tests.c Various bug and feature tests. Compiled and run by make test. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_GETOPT_H #include #endif #include #include #include #include #include "fallback.h" #include "util.h" #include "common.h" #include "proc.h" #include "reader.h" #include "builtin.h" #include "function.h" #include "autoload.h" #include "complete.h" #include "wutil.h" #include "env.h" #include "expand.h" #include "parser.h" #include "tokenizer.h" #include "output.h" #include "exec.h" #include "event.h" #include "path.h" #include "history.h" #include "highlight.h" #include "iothread.h" #include "postfork.h" #include "signal.h" /** The number of tests to run */ //#define ESCAPE_TEST_COUNT 1000000 #define ESCAPE_TEST_COUNT 10000 /** The average length of strings to unescape */ #define ESCAPE_TEST_LENGTH 100 /** The higest character number of character to try and escape */ #define ESCAPE_TEST_CHAR 4000 /** Number of laps to run performance testing loop */ #define LAPS 50 /** The result of one of the test passes */ #define NUM_ANS L"-7 99999999 1234567 deadbeef DEADBEEFDEADBEEF" /** Number of encountered errors */ static int err_count=0; /** Print formatted output */ static void say( const wchar_t *blah, ... ) { va_list va; va_start( va, blah ); vwprintf( blah, va ); va_end( va ); wprintf( L"\n" ); } /** Print formatted error string */ static void err( const wchar_t *blah, ... ) { va_list va; va_start( va, blah ); err_count++; wprintf( L"Error: " ); vwprintf( blah, va ); va_end( va ); wprintf( L"\n" ); } /** Test the escaping/unescaping code by escaping/unescaping random strings and verifying that the original string comes back. */ static void test_escape() { int i; wcstring sb; say( L"Testing escaping and unescaping" ); for( i=0; i sb; say( L"Testing wide/narrow string conversion" ); for( i=0; i&1 'nested \"quoted\" '(string containing subshells ){and,brackets}$as[$well (as variable arrays)]"; const int types[] = { TOK_STRING, TOK_REDIRECT_IN, TOK_STRING, TOK_REDIRECT_FD, TOK_STRING, TOK_STRING, TOK_END } ; size_t i; say( L"Test correct tokenization" ); for( i=0, tok_init( &t, str, 0 ); i<(sizeof(types)/sizeof(int)); i++,tok_next( &t ) ) { if( types[i] != tok_last_type( &t ) ) { err( L"Tokenization error:"); wprintf( L"Token number %d of string \n'%ls'\n, expected token type %ls, got token '%ls' of type %ls\n", i+1, str, tok_get_desc(types[i]), tok_last(&t), tok_get_desc(tok_last_type( &t )) ); } } } } static int test_fork_helper(void *unused) { size_t i; for (i=0; i < 100000; i++) { delete [] (new char[4 * 1024 * 1024]); } return 0; } static void test_fork(void) { return; // Test is disabled until I can force it to fail say(L"Testing fork"); size_t i, max = 100; for (i=0; i < 100; i++) { printf("%lu / %lu\n", i+1, max); /* Do something horrible to try to trigger an error */ #define THREAD_COUNT 8 #define FORK_COUNT 200 #define FORK_LOOP_COUNT 16 signal_block(); for (size_t i=0; i < THREAD_COUNT; i++) { iothread_perform(test_fork_helper, NULL, NULL); } for (size_t q = 0; q < FORK_LOOP_COUNT; q++) { size_t pids[FORK_COUNT]; for (size_t i=0; i < FORK_COUNT; i++) { pid_t pid = execute_fork(false); if (pid > 0) { /* Parent */ pids[i] = pid; } else if (pid == 0) { /* Child */ new char[4 * 1024 * 1024]; exit_without_destructors(0); } else { perror("fork"); } } for (size_t i=0; i < FORK_COUNT; i++) { int status = 0; if (pids[i] != waitpid(pids[i], &status, 0)) { perror("waitpid"); assert(0); } assert(WIFEXITED(status) && 0 == WEXITSTATUS(status)); } } iothread_drain_all(); signal_unblock(); } #undef FORK_COUNT } /** Test the parser */ static void test_parser() { say( L"Testing parser" ); parser_t parser(PARSER_TYPE_GENERAL); say( L"Testing null input to parser" ); if( !parser.test( 0, 0, 0, 0 ) ) { err( L"Null input to parser.test undetected" ); } say( L"Testing block nesting" ); if( !parser.test( L"if; end", 0, 0, 0 ) ) { err( L"Incomplete if statement undetected" ); } if( !parser.test( L"if test; echo", 0, 0, 0 ) ) { err( L"Missing end undetected" ); } if( !parser.test( L"if test; end; end", 0, 0, 0 ) ) { err( L"Unbalanced end undetected" ); } say( L"Testing detection of invalid use of builtin commands" ); if( !parser.test( L"case foo", 0, 0, 0 ) ) { err( L"'case' command outside of block context undetected" ); } if( !parser.test( L"switch ggg; if true; case foo;end;end", 0, 0, 0 ) ) { err( L"'case' command outside of switch block context undetected" ); } if( !parser.test( L"else", 0, 0, 0 ) ) { err( L"'else' command outside of conditional block context undetected" ); } if( !parser.test( L"break", 0, 0, 0 ) ) { err( L"'break' command outside of loop block context undetected" ); } if( !parser.test( L"exec ls|less", 0, 0, 0 ) || !parser.test( L"echo|return", 0, 0, 0 )) { err( L"Invalid pipe command undetected" ); } say( L"Testing basic evaluation" ); #if 0 /* This fails now since the parser takes a wcstring&, and NULL converts to wchar_t * converts to wcstring which crashes (thanks C++) */ if( !parser.eval( 0, 0, TOP ) ) { err( L"Null input when evaluating undetected" ); } #endif if( !parser.eval( L"ls", 0, WHILE ) ) { err( L"Invalid block mode when evaluating undetected" ); } } class lru_node_test_t : public lru_node_t { public: lru_node_test_t(const wcstring &tmp) : lru_node_t(tmp) { } }; class test_lru_t : public lru_cache_t { public: test_lru_t() : lru_cache_t(16) { } std::vector evicted_nodes; virtual void node_was_evicted(lru_node_test_t *node) { assert(find(evicted_nodes.begin(), evicted_nodes.end(), node) == evicted_nodes.end()); evicted_nodes.push_back(node); } }; static void test_lru(void) { say( L"Testing LRU cache" ); test_lru_t cache; std::vector expected_evicted; size_t total_nodes = 20; for (size_t i=0; i < total_nodes; i++) { assert(cache.size() == std::min(i, (size_t)16)); lru_node_test_t *node = new lru_node_test_t(format_val(i)); if (i < 4) expected_evicted.push_back(node); // Adding the node the first time should work, and subsequent times should fail assert(cache.add_node(node)); assert(! cache.add_node(node)); } assert(cache.evicted_nodes == expected_evicted); cache.evict_all_nodes(); assert(cache.evicted_nodes.size() == total_nodes); while (! cache.evicted_nodes.empty()) { lru_node_t *node = cache.evicted_nodes.back(); cache.evicted_nodes.pop_back(); delete node; } } /** Perform parameter expansion and test if the output equals the zero-terminated parameter list supplied. \param in the string to expand \param flags the flags to send to expand_string */ static int expand_test( const wchar_t *in, int flags, ... ) { std::vector output; va_list va; size_t i=0; int res=1; wchar_t *arg; if( expand_string( in, output, flags) ) { } va_start( va, flags ); while( (arg=va_arg(va, wchar_t *) )!= 0 ) { if( output.size() == i ) { res=0; break; } if (output.at(i).completion != arg) { res=0; break; } i++; } va_end( va ); return res; } /** Test globbing and other parameter expansion */ static void test_expand() { say( L"Testing parameter expansion" ); if( !expand_test( L"foo", 0, L"foo", 0 )) { err( L"Strings do not expand to themselves" ); } if( !expand_test( L"a{b,c,d}e", 0, L"abe", L"ace", L"ade", 0 ) ) { err( L"Bracket expansion is broken" ); } if( !expand_test( L"a*", EXPAND_SKIP_WILDCARDS, L"a*", 0 ) ) { err( L"Cannot skip wildcard expansion" ); } } /** Test path functions */ static void test_path() { say( L"Testing path functions" ); wcstring path = L"//foo//////bar/"; wcstring canon = path; path_make_canonical(canon); if( canon != L"/foo/bar" ) { err( L"Bug in canonical PATH code" ); } } /** Testing colors */ static void test_colors() { say(L"Testing colors"); assert(rgb_color_t(L"#FF00A0").is_rgb()); assert(rgb_color_t(L"FF00A0").is_rgb()); assert(rgb_color_t(L"#F30").is_rgb()); assert(rgb_color_t(L"F30").is_rgb()); assert(rgb_color_t(L"f30").is_rgb()); assert(rgb_color_t(L"#FF30a5").is_rgb()); assert(rgb_color_t(L"3f30").is_none()); assert(rgb_color_t(L"##f30").is_none()); assert(rgb_color_t(L"magenta").is_named()); assert(rgb_color_t(L"MaGeNTa").is_named()); assert(rgb_color_t(L"mooganta").is_none()); } /* Testing autosuggestion */ static void test_autosuggest() { bool autosuggest_handle_special(const wcstring &str, const wcstring &working_directory, bool *outSuggestionOK); } /** Test speed of completion calculations */ void perf_complete() { wchar_t c; std::vector out; long long t1, t2; int matches=0; double t; wchar_t str[3]= { 0, 0, 0 } ; int i; say( L"Testing completion performance" ); reader_push(L""); say( L"Here we go" ); t1 = get_time(); for( c=L'a'; c<=L'z'; c++ ) { str[0]=c; reader_set_buffer( str, 0 ); complete( str, out, COMPLETE_DEFAULT, NULL ); matches += out.size(); out.clear(); } t2=get_time(); t = (double)(t2-t1)/(1000000*26); say( L"One letter command completion took %f seconds per completion, %f microseconds/match", t, (double)(t2-t1)/matches ); matches=0; t1 = get_time(); for( i=0; i before, after; history.clear(); size_t i, max = 100; for (i=1; i <= max; i++) { /* Generate a value */ wcstring value = wcstring(L"test item ") + format_val(i); /* Maybe add some backslashes */ if (i % 3 == 0) value.append(L"(slashies \\\\\\ slashies)"); /* Generate some paths */ path_list_t paths; size_t count = rand() % 6; while (count--) { paths.push_front(random_string()); } /* Record this item */ history_item_t item(value, time(NULL), paths); before.push_back(item); history.add(item); } history.save(); /* Read items back in reverse order and ensure they're the same */ for (i=100; i >= 1; i--) { history_item_t item = history.item_at_index(i); assert(! item.empty()); after.push_back(item); } assert(before.size() == after.size()); for (size_t i=0; i < before.size(); i++) { const history_item_t &bef = before.at(i), &aft = after.at(i); assert(bef.contents == aft.contents); assert(bef.creation_timestamp == aft.creation_timestamp); assert(bef.required_paths == aft.required_paths); } } /** Main test */ int main( int argc, char **argv ) { setlocale( LC_ALL, "" ); srand( time( 0 ) ); program_name=L"(ignore)"; say( L"Testing low-level functionality"); say( L"Lines beginning with '(ignore):' are not errors, they are warning messages\ngenerated by the fish parser library when given broken input, and can be\nignored. All actual errors begin with 'Error:'." ); set_main_thread(); proc_init(); event_init(); function_init(); builtin_init(); reader_init(); env_init(); test_format(); test_escape(); test_convert(); test_tok(); test_fork(); test_parser(); test_lru(); test_expand(); test_path(); test_colors(); test_autosuggest(); history_tests_t::test_history(); say( L"Encountered %d errors in low-level tests", err_count ); /* Skip performance tests for now, since they seem to hang when running from inside make (?) */ // say( L"Testing performance" ); // perf_complete(); env_destroy(); reader_destroy(); builtin_destroy(); wutil_destroy(); event_destroy(); proc_destroy(); }