/** \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" #include "highlight.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)] not_a_redirect^ ^ ^^is_a_redirect"; const int types[] = { TOK_STRING, TOK_REDIRECT_IN, TOK_STRING, TOK_REDIRECT_FD, TOK_STRING, TOK_STRING, TOK_STRING, TOK_REDIRECT_OUT, TOK_REDIRECT_APPEND, TOK_STRING, TOK_END }; say(L"Test correct tokenization"); tokenizer_t t(str, 0); for (size_t i=0; i < sizeof types / sizeof *types; 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 < 1000; i++) { //delete [](new char[4 * 1024 * 1024]); for (int j=0; j < 1024; j++) { strerror(j); } } return 0; } static void test_fork(void) { return; 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 10 #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]; for (size_t i=0; i < 1024 * 16; i++) { for (int j=0; j < 256; j++) { strerror(j); } } 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(NULL)) { err(L"Null input to parser.test undetected"); } say(L"Testing block nesting"); if (!parser.test(L"if; end")) { err(L"Incomplete if statement undetected"); } if (!parser.test(L"if test; echo")) { err(L"Missing end undetected"); } if (!parser.test(L"if test; end; end")) { err(L"Unbalanced end undetected"); } say(L"Testing detection of invalid use of builtin commands"); if (!parser.test(L"case foo")) { err(L"'case' command outside of block context undetected"); } if (!parser.test(L"switch ggg; if true; case foo;end;end")) { err(L"'case' command outside of switch block context undetected"); } if (!parser.test(L"else")) { err(L"'else' command outside of conditional block context undetected"); } if (!parser.test(L"else if")) { err(L"'else if' command outside of conditional block context undetected"); } if (!parser.test(L"if false; else if; end")) { err(L"'else if' missing command undetected"); } if (!parser.test(L"break")) { err(L"'break' command outside of loop block context undetected"); } if (!parser.test(L"exec ls|less") || !parser.test(L"echo|return")) { 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", io_chain_t(), 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)) { } #if 0 for (size_t idx=0; idx < output.size(); idx++) { printf("%ls\n", output.at(idx).completion.c_str()); } #endif 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"); } if (system("mkdir -p /tmp/fish_expand_test/")) err(L"mkdir failed"); if (system("touch /tmp/fish_expand_test/.foo")) err(L"touch failed"); if (system("touch /tmp/fish_expand_test/bar")) err(L"touch failed"); // This is checking that .* does NOT match . and .. (https://github.com/fish-shell/fish-shell/issues/270). But it does have to match literal components (e.g. "./*" has to match the same as "*" if (! expand_test(L"/tmp/fish_expand_test/.*", 0, L"/tmp/fish_expand_test/.foo", 0)) { err(L"Expansion not correctly handling dotfiles"); } if (! expand_test(L"/tmp/fish_expand_test/./.*", 0, L"/tmp/fish_expand_test/./.foo", 0)) { err(L"Expansion not correctly handling literal path components in dotfiles"); } //system("rm -Rf /tmp/fish_expand_test"); } /** 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"); } path = L"/"; path_make_canonical(path); if (path != L"/") { err(L"Bug in canonical PATH code"); } } enum word_motion_t { word_motion_left, word_motion_right }; static void test_1_word_motion(word_motion_t motion, move_word_style_t style, const wcstring &test) { wcstring command; std::set stops; // Carets represent stops and should be cut out of the command for (size_t i=0; i < test.size(); i++) { wchar_t wc = test.at(i); if (wc == L'^') { stops.insert(command.size()); } else { command.push_back(wc); } } size_t idx, end; if (motion == word_motion_left) { idx = command.size(); end = 0; } else { idx = 0; end = command.size(); } move_word_state_machine_t sm(style); while (idx != end) { size_t char_idx = (motion == word_motion_left ? idx - 1 : idx); wchar_t wc = command.at(char_idx); bool will_stop = ! sm.consume_char(wc); //printf("idx %lu, looking at %lu (%c): %d\n", idx, char_idx, (char)wc, will_stop); bool expected_stop = (stops.count(idx) > 0); if (will_stop != expected_stop) { wcstring tmp = command; tmp.insert(idx, L"^"); const char *dir = (motion == word_motion_left ? "left" : "right"); if (will_stop) { err(L"Word motion: moving %s, unexpected stop at idx %lu: '%ls'", dir, idx, tmp.c_str()); } else if (! will_stop && expected_stop) { err(L"Word motion: moving %s, should have stopped at idx %lu: '%ls'", dir, idx, tmp.c_str()); } } // We don't expect to stop here next time if (expected_stop) { stops.erase(idx); } if (will_stop) { sm.reset(); } else { idx += (motion == word_motion_left ? -1 : 1); } } } /** Test word motion (forward-word, etc.). Carets represent cursor stops. */ static void test_word_motion() { say(L"Testing word motion"); test_1_word_motion(word_motion_left, move_word_style_punctuation, L"^echo ^hello_^world.^txt"); test_1_word_motion(word_motion_right, move_word_style_punctuation, L"echo^ hello^_world^.txt^"); test_1_word_motion(word_motion_left, move_word_style_punctuation, L"echo ^foo_^foo_^foo/^/^/^/^/^ "); test_1_word_motion(word_motion_right, move_word_style_punctuation, L"echo^ foo^_foo^_foo^/^/^/^/^/ ^"); test_1_word_motion(word_motion_left, move_word_style_path_components, L"^/^foo/^bar/^baz/"); test_1_word_motion(word_motion_left, move_word_style_path_components, L"^echo ^--foo ^--bar"); test_1_word_motion(word_motion_left, move_word_style_path_components, L"^echo ^hi ^> /^dev/^null"); test_1_word_motion(word_motion_left, move_word_style_path_components, L"^echo /^foo/^bar{^aaa,^bbb,^ccc}^bak/"); } /** 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, PATH_REQUIRE_DIR, &tmp) && tmp == L"alpha/"); assert(is_potential_path(L"alpha/", wds, PATH_REQUIRE_DIR, &tmp) && tmp == L"alpha/"); assert(is_potential_path(L"aard", wds, 0, &tmp) && tmp == L"aardvark"); assert(! is_potential_path(L"balpha/", wds, PATH_REQUIRE_DIR, &tmp)); assert(! is_potential_path(L"aard", wds, PATH_REQUIRE_DIR, &tmp)); assert(! is_potential_path(L"aarde", wds, PATH_REQUIRE_DIR, &tmp)); assert(! is_potential_path(L"aarde", wds, 0, &tmp)); assert(is_potential_path(L"/tmp/is_potential_path_test/aardvark", wds, 0, &tmp) && tmp == L"/tmp/is_potential_path_test/aardvark"); assert(is_potential_path(L"/tmp/is_potential_path_test/al", wds, PATH_REQUIRE_DIR, &tmp) && tmp == L"/tmp/is_potential_path_test/alpha/"); assert(is_potential_path(L"/tmp/is_potential_path_test/aardv", wds, 0, &tmp) && tmp == L"/tmp/is_potential_path_test/aardvark"); assert(! is_potential_path(L"/tmp/is_potential_path_test/aardvark", wds, PATH_REQUIRE_DIR, &tmp)); assert(! is_potential_path(L"/tmp/is_potential_path_test/al/", wds, 0, &tmp)); assert(! is_potential_path(L"/tmp/is_potential_path_test/ar", wds, 0, &tmp)); assert(is_potential_path(L"/usr", wds, PATH_REQUIRE_DIR, &tmp) && tmp == L"/usr/"); } /** Test the 'test' builtin */ int builtin_test(parser_t &parser, wchar_t **argv); static bool run_one_test_test(int expected, wcstring_list_t &lst, bool bracket) { parser_t parser(PARSER_TYPE_GENERAL, true); size_t i, count = lst.size(); wchar_t **argv = new wchar_t *[count+3]; argv[0] = (wchar_t *)(bracket ? L"[" : L"test"); for (i=0; i < count; i++) { argv[i+1] = (wchar_t *)lst.at(i).c_str(); } if (bracket) { argv[i+1] = (wchar_t *)L"]"; i++; } 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)); bool bracket = run_one_test_test(expected, lst, true); bool nonbracket = run_one_test_test(expected, lst, false); assert(bracket == nonbracket); return nonbracket; } static void test_test_brackets() { // Ensure [ knows it needs a ] parser_t parser(PARSER_TYPE_GENERAL, true); const wchar_t *argv1[] = {L"[", L"foo", NULL}; assert(builtin_test(parser, (wchar_t **)argv1) != 0); const wchar_t *argv2[] = {L"[", L"foo", L"]", NULL}; assert(builtin_test(parser, (wchar_t **)argv2) == 0); const wchar_t *argv3[] = {L"[", L"foo", L"]", L"bar", NULL}; assert(builtin_test(parser, (wchar_t **)argv3) != 0); } static void test_test() { say(L"Testing test builtin"); test_test_brackets(); 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")); /* These should be errors */ assert(run_test_test(1, L"foo bar")); assert(run_test_test(1, L"foo bar baz")); /* This crashed */ assert(run_test_test(1, L"1 = 1 -a = 1")); } /** 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()); } static void perform_one_autosuggestion_test(const wcstring &command, const wcstring &wd, const wcstring &expected, long line) { wcstring suggestion; bool success = autosuggest_suggest_special(command, wd, suggestion); if (! success) { printf("line %ld: autosuggest_suggest_special() failed for command %ls\n", line, command.c_str()); assert(success); } if (suggestion != expected) { printf("line %ld: autosuggest_suggest_special() returned the wrong expected string for command %ls\n", line, command.c_str()); printf(" actual: %ls\n", suggestion.c_str()); printf("expected: %ls\n", expected.c_str()); assert(suggestion == expected); } } /* Testing test_autosuggest_suggest_special, in particular for properly handling quotes and backslashes */ static void test_autosuggest_suggest_special() { if (system("mkdir -p '/tmp/autosuggest_test/0foobar'")) err(L"mkdir failed"); if (system("mkdir -p '/tmp/autosuggest_test/1foo bar'")) err(L"mkdir failed"); if (system("mkdir -p '/tmp/autosuggest_test/2foo bar'")) err(L"mkdir failed"); if (system("mkdir -p '/tmp/autosuggest_test/3foo\\bar'")) err(L"mkdir failed"); if (system("mkdir -p /tmp/autosuggest_test/4foo\\'bar")) err(L"mkdir failed"); //a path with a single quote if (system("mkdir -p /tmp/autosuggest_test/5foo\\\"bar")) err(L"mkdir failed"); //a path with a double quote if (system("mkdir -p ~/test_autosuggest_suggest_special/")) err(L"mkdir failed"); //make sure tilde is handled const wcstring wd = L"/tmp/autosuggest_test/"; perform_one_autosuggestion_test(L"cd /tmp/autosuggest_test/0", wd, L"cd /tmp/autosuggest_test/0foobar/", __LINE__); perform_one_autosuggestion_test(L"cd \"/tmp/autosuggest_test/0", wd, L"cd \"/tmp/autosuggest_test/0foobar/\"", __LINE__); perform_one_autosuggestion_test(L"cd '/tmp/autosuggest_test/0", wd, L"cd '/tmp/autosuggest_test/0foobar/'", __LINE__); perform_one_autosuggestion_test(L"cd 0", wd, L"cd 0foobar/", __LINE__); perform_one_autosuggestion_test(L"cd \"0", wd, L"cd \"0foobar/\"", __LINE__); perform_one_autosuggestion_test(L"cd '0", wd, L"cd '0foobar/'", __LINE__); perform_one_autosuggestion_test(L"cd /tmp/autosuggest_test/1", wd, L"cd /tmp/autosuggest_test/1foo\\ bar/", __LINE__); perform_one_autosuggestion_test(L"cd \"/tmp/autosuggest_test/1", wd, L"cd \"/tmp/autosuggest_test/1foo bar/\"", __LINE__); perform_one_autosuggestion_test(L"cd '/tmp/autosuggest_test/1", wd, L"cd '/tmp/autosuggest_test/1foo bar/'", __LINE__); perform_one_autosuggestion_test(L"cd 1", wd, L"cd 1foo\\ bar/", __LINE__); perform_one_autosuggestion_test(L"cd \"1", wd, L"cd \"1foo bar/\"", __LINE__); perform_one_autosuggestion_test(L"cd '1", wd, L"cd '1foo bar/'", __LINE__); perform_one_autosuggestion_test(L"cd /tmp/autosuggest_test/2", wd, L"cd /tmp/autosuggest_test/2foo\\ \\ bar/", __LINE__); perform_one_autosuggestion_test(L"cd \"/tmp/autosuggest_test/2", wd, L"cd \"/tmp/autosuggest_test/2foo bar/\"", __LINE__); perform_one_autosuggestion_test(L"cd '/tmp/autosuggest_test/2", wd, L"cd '/tmp/autosuggest_test/2foo bar/'", __LINE__); perform_one_autosuggestion_test(L"cd 2", wd, L"cd 2foo\\ \\ bar/", __LINE__); perform_one_autosuggestion_test(L"cd \"2", wd, L"cd \"2foo bar/\"", __LINE__); perform_one_autosuggestion_test(L"cd '2", wd, L"cd '2foo bar/'", __LINE__); perform_one_autosuggestion_test(L"cd /tmp/autosuggest_test/3", wd, L"cd /tmp/autosuggest_test/3foo\\\\bar/", __LINE__); perform_one_autosuggestion_test(L"cd \"/tmp/autosuggest_test/3", wd, L"cd \"/tmp/autosuggest_test/3foo\\bar/\"", __LINE__); perform_one_autosuggestion_test(L"cd '/tmp/autosuggest_test/3", wd, L"cd '/tmp/autosuggest_test/3foo\\bar/'", __LINE__); perform_one_autosuggestion_test(L"cd 3", wd, L"cd 3foo\\\\bar/", __LINE__); perform_one_autosuggestion_test(L"cd \"3", wd, L"cd \"3foo\\bar/\"", __LINE__); perform_one_autosuggestion_test(L"cd '3", wd, L"cd '3foo\\bar/'", __LINE__); perform_one_autosuggestion_test(L"cd /tmp/autosuggest_test/4", wd, L"cd /tmp/autosuggest_test/4foo\\'bar/", __LINE__); perform_one_autosuggestion_test(L"cd \"/tmp/autosuggest_test/4", wd, L"cd \"/tmp/autosuggest_test/4foo'bar/\"", __LINE__); perform_one_autosuggestion_test(L"cd '/tmp/autosuggest_test/4", wd, L"cd '/tmp/autosuggest_test/4foo\\'bar/'", __LINE__); perform_one_autosuggestion_test(L"cd 4", wd, L"cd 4foo\\'bar/", __LINE__); perform_one_autosuggestion_test(L"cd \"4", wd, L"cd \"4foo'bar/\"", __LINE__); perform_one_autosuggestion_test(L"cd '4", wd, L"cd '4foo\\'bar/'", __LINE__); perform_one_autosuggestion_test(L"cd /tmp/autosuggest_test/5", wd, L"cd /tmp/autosuggest_test/5foo\\\"bar/", __LINE__); perform_one_autosuggestion_test(L"cd \"/tmp/autosuggest_test/5", wd, L"cd \"/tmp/autosuggest_test/5foo\\\"bar/\"", __LINE__); perform_one_autosuggestion_test(L"cd '/tmp/autosuggest_test/5", wd, L"cd '/tmp/autosuggest_test/5foo\"bar/'", __LINE__); perform_one_autosuggestion_test(L"cd 5", wd, L"cd 5foo\\\"bar/", __LINE__); perform_one_autosuggestion_test(L"cd \"5", wd, L"cd \"5foo\\\"bar/\"", __LINE__); perform_one_autosuggestion_test(L"cd '5", wd, L"cd '5foo\"bar/'", __LINE__); perform_one_autosuggestion_test(L"cd ~/test_autosuggest_suggest_specia", wd, L"cd ~/test_autosuggest_suggest_special/", __LINE__); // A single quote should defeat tilde expansion perform_one_autosuggestion_test(L"cd '~/test_autosuggest_suggest_specia'", wd, L"", __LINE__); system("rm -Rf '/tmp/autosuggest_test/'"); system("rm -Rf ~/test_autosuggest_suggest_special/"); } static void test_autosuggestion_combining() { say(L"Testing autosuggestion combining"); assert(combine_command_and_autosuggestion(L"alpha", L"alphabeta") == L"alphabeta"); // when the last token contains no capital letters, we use the case of the autosuggestion assert(combine_command_and_autosuggestion(L"alpha", L"ALPHABETA") == L"ALPHABETA"); // when the last token contains capital letters, we use its case assert(combine_command_and_autosuggestion(L"alPha", L"alphabeTa") == L"alPhabeTa"); // if autosuggestion is not longer than input, use the input's case assert(combine_command_and_autosuggestion(L"alpha", L"ALPHAA") == L"ALPHAA"); assert(combine_command_and_autosuggestion(L"alpha", L"ALPHA") == L"alpha"); } /** 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 void test_history_formats(void); static void test_history_speed(void); static void test_history_races(void); static void test_history_races_pound_on_history(); }; 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 item removal */ history.remove(L"Alpha"); history_search_t search3(history, L"Alpha"); test_history_matches(search3, 0); /* 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_back(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); } static wcstring_list_t generate_history_lines(int pid) { wcstring_list_t result; long max = 256; result.reserve(max); for (long i=0; i < max; i++) { result.push_back(format_string(L"%ld %ld", (long)pid, i)); } return result; } void history_tests_t::test_history_races_pound_on_history() { /* Called in child process to modify history */ history_t *hist = new history_t(L"race_test"); hist->chaos_mode = true; const wcstring_list_t lines = generate_history_lines(getpid()); for (size_t idx = 0; idx < lines.size(); idx++) { const wcstring &line = lines.at(idx); hist->add(line); hist->save(); } delete hist; } void history_tests_t::test_history_races(void) { say(L"Testing history race conditions"); // Ensure history is clear history_t *hist = new history_t(L"race_test"); hist->clear(); delete hist; // Test concurrent history writing #define RACE_COUNT 10 pid_t children[RACE_COUNT]; for (size_t i=0; i < RACE_COUNT; i++) { pid_t pid = fork(); if (! pid) { // Child process setup_fork_guards(); test_history_races_pound_on_history(); exit_without_destructors(0); } else { // Parent process children[i] = pid; } } // Wait for all children for (size_t i=0; i < RACE_COUNT; i++) { int stat; waitpid(children[i], &stat, WUNTRACED); } // Compute the expected lines wcstring_list_t lines[RACE_COUNT]; for (size_t i=0; i < RACE_COUNT; i++) { lines[i] = generate_history_lines(children[i]); } // Count total lines size_t line_count = 0; for (size_t i=0; i < RACE_COUNT; i++) { line_count += lines[i].size(); } // Ensure we consider the lines that have been outputted as part of our history time_barrier(); /* Ensure that we got sane, sorted results */ hist = new history_t(L"race_test"); hist->chaos_mode = true; size_t hist_idx; for (hist_idx = 1; ; hist_idx ++) { history_item_t item = hist->item_at_index(hist_idx); if (item.empty()) break; // The item must be present in one of our 'lines' arrays // If it is present, then every item after it is assumed to be missed size_t i; for (i=0; i < RACE_COUNT; i++) { wcstring_list_t::iterator where = std::find(lines[i].begin(), lines[i].end(), item.str()); if (where != lines[i].end()) { // Delete everything from the found location onwards lines[i].resize(where - lines[i].begin()); // Break because we found it break; } } if (i >= RACE_COUNT) { err(L"Line '%ls' found in history not found in some array", item.str().c_str()); } } // every write should add at least one item assert(hist_idx >= RACE_COUNT); //hist->clear(); delete hist; } 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 } static bool install_sample_history(const wchar_t *name) { char command[512]; snprintf(command, sizeof command, "cp tests/%ls ~/.config/fish/%ls_history", name, name); if (system(command)) { err(L"Failed to copy sample history"); return false; } return true; } /* Indicates whether the history is equal to the given null-terminated array of strings. */ static bool history_equals(history_t &hist, const wchar_t * const *strings) { /* Count our expected items */ size_t expected_count = 0; while (strings[expected_count]) { expected_count++; } /* Ensure the contents are the same */ size_t history_idx = 1; size_t array_idx = 0; for (;;) { const wchar_t *expected = strings[array_idx]; history_item_t item = hist.item_at_index(history_idx); if (expected == NULL) { if (! item.empty()) { err(L"Expected empty item at history index %lu", history_idx); } break; } else { if (item.str() != expected) { err(L"Expected '%ls', found '%ls' at index %lu", expected, item.str().c_str(), history_idx); } } history_idx++; array_idx++; } return true; } void history_tests_t::test_history_formats(void) { const wchar_t *name; // Test inferring and reading legacy and bash history formats name = L"history_sample_fish_1_x"; say(L"Testing %ls", name); if (! install_sample_history(name)) { err(L"Couldn't open file tests/%ls", name); } else { /* Note: This is backwards from what appears in the file */ const wchar_t * const expected[] = { L"#def", L"echo #abc", L"function yay\n" "echo hi\n" "end", L"cd foobar", L"ls /", NULL }; history_t &test_history = history_t::history_with_name(name); if (! history_equals(test_history, expected)) { err(L"test_history_formats failed for %ls\n", name); } test_history.clear(); } name = L"history_sample_fish_2_0"; say(L"Testing %ls", name); if (! install_sample_history(name)) { err(L"Couldn't open file tests/%ls", name); } else { const wchar_t * const expected[] = { L"echo this has\\\nbackslashes", L"function foo\n" "echo bar\n" "end", L"echo alpha", NULL }; history_t &test_history = history_t::history_with_name(name); if (! history_equals(test_history, expected)) { err(L"test_history_formats failed for %ls\n", name); } test_history.clear(); } say(L"Testing bash import"); FILE *f = fopen("tests/history_sample_bash", "r"); if (! f) { err(L"Couldn't open file tests/history_sample_bash"); } else { // It should skip over the export command since that's a bash-ism const wchar_t *expected[] = { L"echo supsup", L"history --help", L"echo foo", NULL }; history_t &test_history = history_t::history_with_name(L"bash_import"); test_history.populate_from_bash(f); if (! history_equals(test_history, expected)) { err(L"test_history_formats failed for bash import\n"); } test_history.clear(); fclose(f); } } void history_tests_t::test_history_speed(void) { say(L"Testing history speed"); history_t *hist = new history_t(L"speed_test"); wcstring item = L"History Speed Test - X"; /* Test for 10 seconds */ double start = timef(); double end = start + 4; double stop = 0; size_t count = 0; for (;;) { item[item.size() - 1] = L'0' + (count % 10); hist->add(item); count++; stop = timef(); if (stop >= end) break; } printf("%lu items - %.2f msec per item\n", (unsigned long)count, (stop - start) * 1E6 / count); hist->clear(); delete hist; } /** 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_convert_nulls(); test_tok(); test_fork(); test_parser(); test_lru(); test_expand(); test_test(); test_path(); test_word_motion(); test_is_potential_path(); test_colors(); test_autosuggestion_combining(); test_autosuggest_suggest_special(); history_tests_t::test_history(); history_tests_t::test_history_merge(); history_tests_t::test_history_races(); history_tests_t::test_history_formats(); //history_tests_t::test_history_speed(); 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(); }