/** \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 #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++) { pid_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, true); 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(to_string(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" ); } } /** Test is_potential_path */ static void test_is_potential_path() { say(L"Testing is_potential_path"); if (system("rm -Rf /tmp/is_potential_path_test/")) { err(L"Failed to remove /tmp/is_potential_path_test/"); } /* Directories */ if (system("mkdir -p /tmp/is_potential_path_test/alpha/")) err(L"mkdir failed"); if (system("mkdir -p /tmp/is_potential_path_test/beta/")) err(L"mkdir failed"); /* Files */ if (system("touch /tmp/is_potential_path_test/aardvark")) err(L"touch failed"); if (system("touch /tmp/is_potential_path_test/gamma")) err(L"touch failed"); const wcstring wd = L"/tmp/is_potential_path_test/"; const wcstring_list_t wds(1, wd); wcstring tmp; assert(is_potential_path(L"al", wds, true, &tmp) && tmp == L"alpha/"); assert(is_potential_path(L"alpha/", wds, true, &tmp) && tmp == L"alpha/"); assert(is_potential_path(L"aard", wds, false, &tmp) && tmp == L"aardvark"); assert(! is_potential_path(L"balpha/", wds, true, &tmp)); assert(! is_potential_path(L"aard", wds, true, &tmp)); assert(! is_potential_path(L"aarde", wds, true, &tmp)); assert(! is_potential_path(L"aarde", wds, false, &tmp)); assert(is_potential_path(L"/tmp/is_potential_path_test/aardvark", wds, false, &tmp) && tmp == L"/tmp/is_potential_path_test/aardvark"); assert(is_potential_path(L"/tmp/is_potential_path_test/al", wds, true, &tmp) && tmp == L"/tmp/is_potential_path_test/alpha/"); assert(is_potential_path(L"/tmp/is_potential_path_test/aardv", wds, false, &tmp) && tmp == L"/tmp/is_potential_path_test/aardvark"); assert(! is_potential_path(L"/tmp/is_potential_path_test/aardvark", wds, true, &tmp)); assert(! is_potential_path(L"/tmp/is_potential_path_test/al/", wds, false, &tmp)); assert(! is_potential_path(L"/tmp/is_potential_path_test/ar", wds, false, &tmp)); assert(is_potential_path(L"/usr", wds, true, &tmp) && tmp == L"/usr/"); } /** Test the 'test' builtin */ int builtin_test( parser_t &parser, wchar_t **argv ); static bool run_test_test(int expected, wcstring_list_t &lst) { parser_t parser(PARSER_TYPE_GENERAL, true); size_t i, count = lst.size(); wchar_t **argv = new wchar_t *[count+2]; argv[0] = (wchar_t *)L"test"; for (i=0; i < count; i++) { argv[i+1] = (wchar_t *)lst.at(i).c_str(); } argv[i+1] = NULL; int result = builtin_test(parser, argv); delete[] argv; return expected == result; } static bool run_test_test(int expected, const wcstring &str) { using namespace std; wcstring_list_t lst; wistringstream iss(str); copy(istream_iterator >(iss), istream_iterator >(), back_inserter >(lst)); return run_test_test(expected, lst); } static void test_test() { say(L"Testing test builtin"); assert(run_test_test(0, L"5 -ne 6")); assert(run_test_test(0, L"5 -eq 5")); assert(run_test_test(0, L"0 -eq 0")); assert(run_test_test(0, L"-1 -eq -1")); assert(run_test_test(0, L"1 -ne -1")); assert(run_test_test(1, L"-1 -ne -1")); assert(run_test_test(0, L"abc != def")); assert(run_test_test(1, L"abc = def")); assert(run_test_test(0, L"5 -le 10")); assert(run_test_test(0, L"10 -le 10")); assert(run_test_test(1, L"20 -le 10")); assert(run_test_test(0, L"-1 -le 0")); assert(run_test_test(1, L"0 -le -1")); assert(run_test_test(0, L"15 -ge 10")); assert(run_test_test(0, L"15 -ge 10")); assert(run_test_test(1, L"! 15 -ge 10")); assert(run_test_test(0, L"! ! 15 -ge 10")); assert(run_test_test(0, L"0 -ne 1 -a 0 -eq 0")); assert(run_test_test(0, L"0 -ne 1 -a -n 5")); assert(run_test_test(0, L"-n 5 -a 10 -gt 5")); assert(run_test_test(0, L"-n 3 -a -n 5")); /* test precedence: '0 == 0 || 0 == 1 && 0 == 2' should be evaluated as: '0 == 0 || (0 == 1 && 0 == 2)' and therefore true. If it were '(0 == 0 || 0 == 1) && 0 == 2' it would be false. */ assert(run_test_test(0, L"0 = 0 -o 0 = 1 -a 0 = 2")); assert(run_test_test(0, L"-n 5 -o 0 = 1 -a 0 = 2")); assert(run_test_test(1, L"( 0 = 0 -o 0 = 1 ) -a 0 = 2")); assert(run_test_test(0, L"0 = 0 -o ( 0 = 1 -a 0 = 2 )")); /* A few lame tests for permissions; these need to be a lot more complete. */ assert(run_test_test(0, L"-e /bin/ls")); assert(run_test_test(1, L"-e /bin/ls_not_a_path")); assert(run_test_test(0, L"-x /bin/ls")); assert(run_test_test(1, L"-x /bin/ls_not_a_path")); assert(run_test_test(0, L"-d /bin/")); assert(run_test_test(1, L"-d /bin/ls")); /* This failed at one point */ assert(run_test_test(1, L"-d /bin -a 5 -eq 3")); assert(run_test_test(0, L"-d /bin -o 5 -eq 3")); assert(run_test_test(0, L"-d /bin -a ! 5 -eq 3")); /* We didn't properly handle multiple "just strings" either */ assert(run_test_test(0, L"foo")); assert(run_test_test(0, L"foo -a bar")); } /** 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_special_validate_from_history(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; iitem_at_index(i); if (item.empty()) break; if (item.str() == txt) { result = true; break; } } return result; } class history_tests_t { public: static void test_history(void); static void test_history_merge(void); }; static wcstring random_string(void) { wcstring result; size_t max = 1 + rand() % 32; while (max--) { wchar_t c = 1 + rand()%ESCAPE_TEST_CHAR; result.push_back(c); } return result; } void history_tests_t::test_history(void) { say( L"Testing history"); history_t &history = history_t::history_with_name(L"test_history"); history.clear(); history.add(L"Gamma"); history.add(L"Beta"); history.add(L"Alpha"); /* All three items match "a" */ history_search_t search1(history, L"a"); test_history_matches(search1, 3); assert(search1.current_string() == L"Alpha"); /* One item matches "et" */ history_search_t search2(history, L"et"); test_history_matches(search2, 1); assert(search2.current_string() == L"Beta"); /* Test history escaping and unescaping, yaml, etc. */ std::vector before, after; history.clear(); size_t i, max = 100; for (i=1; i <= max; i++) { /* Generate a value */ wcstring value = wcstring(L"test item ") + to_string(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); } /* Clean up after our tests */ history.clear(); } // wait until the next second static void time_barrier(void) { time_t start = time(NULL); do { usleep(1000); } while (time(NULL) == start); } void history_tests_t::test_history_merge(void) { // In a single fish process, only one history is allowed to exist with the given name // But it's common to have multiple history instances with the same name active in different processes, // e.g. when you have multiple shells open. // We try to get that right and merge all their history together. Test that case. say( L"Testing history merge"); const size_t count = 3; const wcstring name = L"merge_test"; history_t *hists[count] = {new history_t(name), new history_t(name), new history_t(name)}; wcstring texts[count] = {L"History 1", L"History 2", L"History 3"}; /* Make sure history is clear */ for (size_t i=0; i < count; i++) { hists[i]->clear(); } /* Make sure we don't add an item in the same second as we created the history */ time_barrier(); /* Add a different item to each */ for (size_t i=0; i < count; i++) { hists[i]->add(texts[i]); } /* Save them */ for (size_t i=0; i < count; i++) { hists[i]->save(); } /* Make sure each history contains what it ought to, but they have not leaked into each other */ for (size_t i = 0; i < count; i++) { for (size_t j=0; j < count; j++) { bool does_contain = history_contains(hists[i], texts[j]); bool should_contain = (i == j); assert(should_contain == does_contain); } } /* Make a new history. It should contain everything. The time_barrier() is so that the timestamp is newer, since we only pick up items whose timestamp is before the birth stamp. */ time_barrier(); history_t *everything = new history_t(name); for (size_t i=0; i < count; i++) { assert(history_contains(everything, texts[i])); } /* Clean up */ for (size_t i=0; i < 3; i++) { delete hists[i]; } everything->clear(); delete everything; //not as scary as it looks } /** Main test */ int main( int argc, char **argv ) { setlocale( LC_ALL, "" ); srand( time( 0 ) ); configure_thread_assertions_for_testing(); 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(); setup_fork_guards(); 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_test(); test_path(); test_is_potential_path(); test_colors(); test_autosuggest(); history_tests_t::test_history(); history_tests_t::test_history_merge(); 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(); }