diff --git a/.cirrus.yml b/.cirrus.yml index 55cf5b1f4..bdf704559 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -48,7 +48,7 @@ linux_task: - (cat /proc/meminfo | grep MemTotal) || true - mkdir build && cd build - cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCTEST_PARALLEL_LEVEL=6 .. - - ninja -j 6 fish fish_tests + - ninja -j 6 fish - ninja fish_run_tests # CI task disabled during RIIR transition @@ -71,7 +71,7 @@ linux_arm_task: - (cat /proc/meminfo | grep MemTotal) || true - mkdir build && cd build - cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCTEST_PARALLEL_LEVEL=6 .. - - ninja -j 6 fish fish_tests + - ninja -j 6 fish - file ./fish - ninja fish_run_tests @@ -112,7 +112,7 @@ freebsd_task: # For some reason, this doesn't do the job: # - sudo -u fish-user sh -c 'echo source \$HOME/.cargo/env >> $HOME/.cshrc' - sudo -u fish-user -s cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCTEST_PARALLEL_LEVEL=1 .. - - sudo -u fish-user sh -c '. $HOME/.cargo/env; ninja -j 6 fish fish_tests' + - sudo -u fish-user sh -c '. $HOME/.cargo/env; ninja -j 6 fish' - sudo -u fish-user sh -c '. $HOME/.cargo/env; ninja fish_run_tests' only_if: $CIRRUS_REPO_OWNER == 'fish-shell' diff --git a/build_tools/find_globals.fish b/build_tools/find_globals.fish index 7cee77ab4..1ac5c5b69 100755 --- a/build_tools/find_globals.fish +++ b/build_tools/find_globals.fish @@ -22,7 +22,6 @@ set -g nm_regex '^([^ ]+) ([dDbB])' set -l total_globals 0 set -l boring_files \ fish_key_reader.cpp.o \ - fish_tests.cpp.o \ fish_indent.cpp.o # return if we should ignore the given symbol name diff --git a/cmake/Tests.cmake b/cmake/Tests.cmake index 4061a6b20..a416ce3b3 100644 --- a/cmake/Tests.cmake +++ b/cmake/Tests.cmake @@ -37,7 +37,7 @@ add_custom_target(fish_run_tests FISH_SOURCE_DIR=${CMAKE_SOURCE_DIR} ${CMAKE_CTEST_COMMAND} --force-new-ctest-process # --verbose --output-on-failure --progress - DEPENDS fish_tests tests_buildroot_target + DEPENDS tests_dir funcs_dir tests_buildroot_target USES_TERMINAL ) @@ -50,26 +50,9 @@ if(POLICY CMP0037) endif() cmake_policy(POP) -# Build the low-level tests code -add_executable(fish_tests EXCLUDE_FROM_ALL - src/fish_tests.cpp) -fish_link_deps_and_sign(fish_tests) - # The "test" directory. set(TEST_DIR ${CMAKE_CURRENT_BINARY_DIR}/test) -# CMake doesn't really support dynamic test discovery where a test harness is executed to list the -# tests it contains, making fish_tests.cpp's tests opaque to CMake (whereas littlecheck tests can be -# enumerated from the filesystem). We used to compile fish_tests.cpp without linking against -# anything (-Wl,-undefined,dynamic_lookup,--unresolved-symbols=ignore-all) to get it to print its -# tests at configuration time, but that's a little too much dark CMake magic. -# -# We now identify tests by checking against a magic regex that's #define'd as a no-op C-side. -file(READ "${CMAKE_SOURCE_DIR}/src/fish_tests.cpp" FISH_TESTS_CPP) -string(REGEX MATCHALL "TEST_GROUP\\( *\"([^\"]+)\"" "LOW_LEVEL_TESTS" "${FISH_TESTS_CPP}") -string(REGEX REPLACE "TEST_GROUP\\( *\"([^\"]+)\"" "\\1" "LOW_LEVEL_TESTS" "${LOW_LEVEL_TESTS}") -list(REMOVE_DUPLICATES LOW_LEVEL_TESTS) - # The directory into which fish is installed. set(TEST_INSTALL_DIR ${TEST_DIR}/buildroot) @@ -93,8 +76,6 @@ if(NOT FISH_IN_TREE_BUILD) ${CMAKE_SOURCE_DIR}/tests/ ${CMAKE_BINARY_DIR}/tests/ COMMENT "Copying test files to binary dir" VERBATIM) - - add_dependencies(fish_tests tests_dir funcs_dir) endif() # Copy littlecheck.py @@ -108,12 +89,12 @@ set(CMAKE_XCODE_GENERATE_SCHEME 0) # CMake being CMake, you can't just add a DEPENDS argument to add_test to make it depend on any of # your binaries actually being built before `make test` is executed (requiring `make all` first), -# and the only dependency a test can have is on another test. So we make building fish and -# `fish_tests` prerequisites to our entire top-level `test` target. +# and the only dependency a test can have is on another test. So we make building fish +# prerequisites to our entire top-level `test` target. function(add_test_target NAME) string(REPLACE "/" "-" NAME ${NAME}) add_custom_target("test_${NAME}" COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure -R "^${NAME}$$" - DEPENDS fish_tests tests_buildroot_target USES_TERMINAL ) + DEPENDS tests_dir funcs_dir tests_buildroot_target USES_TERMINAL ) endfunction() add_custom_target(tests_buildroot_target @@ -138,17 +119,6 @@ else() set(CMAKE_SKIPPED_HACK) endif() -foreach(LTEST ${LOW_LEVEL_TESTS}) - add_test( - NAME ${LTEST} - COMMAND sh ${CMAKE_CURRENT_BINARY_DIR}/tests/test_env.sh - ${CMAKE_BINARY_DIR}/fish_tests ${LTEST} - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - ) - set_tests_properties(${LTEST} PROPERTIES SKIP_RETURN_CODE ${SKIP_RETURN_CODE}) - add_test_target("${LTEST}") -endforeach(LTEST) - FILE(GLOB FISH_CHECKS CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/tests/checks/*.fish) foreach(CHECK ${FISH_CHECKS}) get_filename_component(CHECK_NAME ${CHECK} NAME) diff --git a/debian/rules b/debian/rules index 0696e5070..9202f6ac8 100755 --- a/debian/rules +++ b/debian/rules @@ -12,6 +12,6 @@ override_dh_auto_configure: dh_auto_configure --buildsystem=cmake # On CMake 3.5 (and possibly 3.6), the test target does not pick up its dependencies properly -# Build fish_tests/tests_buildroot_target by hand (remove this once Ubuntu Xenial is out of support) +# Build tests_buildroot_target by hand (remove this once Ubuntu Xenial is out of support) override_dh_auto_build: - dh_auto_build -- all fish_tests tests_buildroot_target + dh_auto_build -- all tests_dir funcs_dir tests_buildroot_target diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp deleted file mode 100644 index 0de0db3b0..000000000 --- a/src/fish_tests.cpp +++ /dev/null @@ -1,914 +0,0 @@ -// Various bug and feature tests. Compiled and run by make test. -#include "config.h" // IWYU pragma: keep - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "fds.rs.h" -#include "parse_constants.rs.h" - -#ifdef FISH_CI_SAN -#include -#endif - -#include "abbrs.h" -#include "ast.h" -#include "color.h" -#include "common.h" -#include "complete.h" -#include "cxxgen.h" -#include "enum_set.h" -#include "env.h" -#include "env/env_ffi.rs.h" -#include "env_universal_common.h" -#include "expand.h" -#include "fallback.h" // IWYU pragma: keep -#include "fd_monitor.rs.h" -#include "fd_readable_set.rs.h" -#include "fds.h" -#include "ffi_baggage.h" -#include "ffi_init.rs.h" -#include "function.h" -#include "future_feature_flags.h" -#include "global_safety.h" -#include "highlight.h" -#include "history.h" -#include "input_ffi.rs.h" -#include "io.h" -#include "iothread.h" -#include "kill.rs.h" -#include "lru.h" -#include "maybe.h" -#include "operation_context.h" -#include "pager.h" -#include "parse_constants.h" -#include "parse_tree.h" -#include "parse_util.h" -#include "parser.h" -#include "path.h" -#include "proc.h" -#include "reader.h" -#include "redirection.h" -#include "screen.h" -#include "signals.h" -#include "termsize.h" -#include "threads.rs.h" -#include "tokenizer.h" -#include "util.h" -#include "wcstringutil.h" -#include "wgetopt.h" -#include "wildcard.h" -#include "wutil.h" // IWYU pragma: keep - -static const char *const *s_arguments; -static int s_test_run_count = 0; - -// Indicate if we should test the given function. Either we test everything (all arguments) or we -// run only tests that have a prefix in s_arguments. -// If \p default_on is set, then allow no args to run this test by default. -static bool should_test_function(const char *func_name, bool default_on = true) { - bool result = false; - if (!s_arguments || !s_arguments[0]) { - // No args, test if defaulted on. - result = default_on; - } else { - for (size_t i = 0; s_arguments[i] != nullptr; i++) { - if (!std::strcmp(func_name, s_arguments[i])) { - result = true; - break; - } - } - } - if (result) s_test_run_count++; - return result; -} - -/// The number of tests to run. -#define ESCAPE_TEST_COUNT 100000 -/// The average length of strings to unescape. -#define ESCAPE_TEST_LENGTH 100 - -/// Number of encountered errors. -static int err_count = 0; - -/// Print formatted output. -static void say(const wchar_t *fmt, ...) { - va_list va; - va_start(va, fmt); - std::vfwprintf(stdout, fmt, va); - va_end(va); - std::fwprintf(stdout, L"\n"); -} - -/// Print formatted error string. -static void err(const wchar_t *blah, ...) { - va_list va; - va_start(va, blah); - err_count++; - - // Show errors in red. - std::fputws(L"\x1B[31m", stdout); - std::fwprintf(stdout, L"Error: "); - std::vfwprintf(stdout, blah, va); - va_end(va); - - // Return to normal color. - std::fputws(L"\x1B[0m", stdout); - std::fwprintf(stdout, L"\n"); -} - -static std::vector pushed_dirs; - -// Helper to return a string whose length greatly exceeds PATH_MAX. -wcstring get_overlong_path() { - wcstring longpath; - longpath.reserve(PATH_MAX * 2 + 10); - while (longpath.size() <= PATH_MAX * 2) { - longpath += L"/overlong"; - } - return longpath; -} - -// The odd formulation of these macros is to avoid "multiple unary operator" warnings from oclint -// were we to use the more natural "if (!(e)) err(..." form. We have to do this because the rules -// for the C preprocessor make it practically impossible to embed a comment in the body of a macro. -#define do_test(e) \ - do { \ - if (e) { \ - ; \ - } else { \ - err(L"Test failed on line %lu: %s", __LINE__, #e); \ - } \ - } while (0) - -#define do_test1(e, msg) \ - do { \ - if (e) { \ - ; \ - } else { \ - err(L"Test failed on line %lu: %ls", __LINE__, (msg)); \ - } \ - } while (0) - -// todo!("already ported, delete this"); -/// Test that the fish functions for converting strings to numbers work. -static void test_str_to_num() { - say(L"Testing str_to_num"); - const wchar_t *end; - int i; - long l; - - i = fish_wcstoi(L""); - do_test1(errno == EINVAL && i == 0, L"converting empty string to int did not fail"); - i = fish_wcstoi(L" \n "); - do_test1(errno == EINVAL && i == 0, L"converting whitespace string to int did not fail"); - i = fish_wcstoi(L"123"); - do_test1(errno == 0 && i == 123, L"converting valid num to int did not succeed"); - i = fish_wcstoi(L"-123"); - do_test1(errno == 0 && i == -123, L"converting valid num to int did not succeed"); - i = fish_wcstoi(L" 345 "); - do_test1(errno == 0 && i == 345, L"converting valid num to int did not succeed"); - i = fish_wcstoi(L" -345 "); - do_test1(errno == 0 && i == -345, L"converting valid num to int did not succeed"); - i = fish_wcstoi(L"x345"); - do_test1(errno == EINVAL && i == 0, L"converting invalid num to int did not fail"); - i = fish_wcstoi(L" x345"); - do_test1(errno == EINVAL && i == 0, L"converting invalid num to int did not fail"); - i = fish_wcstoi(L"456 x"); - do_test1(errno == -1 && i == 456, L"converting invalid num to int did not fail"); - i = fish_wcstoi(L"99999999999999999999999"); - do_test1(errno == ERANGE && i == INT_MAX, L"converting invalid num to int did not fail"); - i = fish_wcstoi(L"-99999999999999999999999"); - do_test1(errno == ERANGE && i == INT_MIN, L"converting invalid num to int did not fail"); - i = fish_wcstoi(L"567]", &end); - do_test1(errno == -1 && i == 567 && *end == L']', - L"converting valid num to int did not succeed"); - // This is subtle. "567" in base 8 is "375" in base 10. The final "8" is not converted. - i = fish_wcstoi(L"5678", &end, 8); - do_test1(errno == -1 && i == 375 && *end == L'8', - L"converting invalid num to int did not fail"); - - l = fish_wcstol(L""); - do_test1(errno == EINVAL && l == 0, L"converting empty string to long did not fail"); - l = fish_wcstol(L" \t "); - do_test1(errno == EINVAL && l == 0, L"converting whitespace string to long did not fail"); - l = fish_wcstol(L"123"); - do_test1(errno == 0 && l == 123, L"converting valid num to long did not succeed"); - l = fish_wcstol(L"-123"); - do_test1(errno == 0 && l == -123, L"converting valid num to long did not succeed"); - l = fish_wcstol(L" 345 "); - do_test1(errno == 0 && l == 345, L"converting valid num to long did not succeed"); - l = fish_wcstol(L" -345 "); - do_test1(errno == 0 && l == -345, L"converting valid num to long did not succeed"); - l = fish_wcstol(L"x345"); - do_test1(errno == EINVAL && l == 0, L"converting invalid num to long did not fail"); - l = fish_wcstol(L" x345"); - do_test1(errno == EINVAL && l == 0, L"converting invalid num to long did not fail"); - l = fish_wcstol(L"456 x"); - do_test1(errno == -1 && l == 456, L"converting invalid num to long did not fail"); - l = fish_wcstol(L"99999999999999999999999"); - do_test1(errno == ERANGE && l == LONG_MAX, L"converting invalid num to long did not fail"); - l = fish_wcstol(L"-99999999999999999999999"); - do_test1(errno == ERANGE && l == LONG_MIN, L"converting invalid num to long did not fail"); - l = fish_wcstol(L"567]", &end); - do_test1(errno == -1 && l == 567 && *end == L']', - L"converting valid num to long did not succeed"); - // This is subtle. "567" in base 8 is "375" in base 10. The final "8" is not converted. - l = fish_wcstol(L"5678", &end, 8); - do_test1(errno == -1 && l == 375 && *end == L'8', - L"converting invalid num to long did not fail"); -} - -enum class test_enum { alpha, beta, gamma, COUNT }; - -template <> -struct enum_info_t { - static constexpr auto count = test_enum::COUNT; -}; - -// todo!("no need to port, delete this"); -static void test_enum_set() { - say(L"Testing enum set"); - enum_set_t es; - do_test(es.none()); - do_test(!es.any()); - do_test(es.to_raw() == 0); - do_test(es == enum_set_t::from_raw(0)); - do_test(es != enum_set_t::from_raw(1)); - - es.set(test_enum::beta); - do_test(es.get(test_enum::beta)); - do_test(!es.get(test_enum::alpha)); - do_test(es & test_enum::beta); - do_test(!(es & test_enum::alpha)); - do_test(es.to_raw() == 2); - do_test(es == enum_set_t::from_raw(2)); - do_test(es == enum_set_t{test_enum::beta}); - do_test(es != enum_set_t::from_raw(3)); - do_test(es.any()); - do_test(!es.none()); - - do_test((enum_set_t{test_enum::beta} | test_enum::alpha).to_raw() == 3); - do_test((enum_set_t{test_enum::beta} | enum_set_t{test_enum::alpha}) - .to_raw() == 3); -} - -// todo!("no need to port, delete this"); -static void test_enum_array() { - say(L"Testing enum array"); - enum_array_t es{}; - do_test(es.size() == enum_count()); - es[test_enum::beta] = "abc"; - do_test(es[test_enum::beta] == "abc"); - es.at(test_enum::gamma) = "def"; - do_test(es.at(test_enum::gamma) == "def"); -} - -/// Helper to convert a narrow string to a sequence of hex digits. -static std::string str2hex(const std::string &input) { - std::string output; - for (char c : input) { - char buff[16]; - size_t amt = snprintf(buff, sizeof buff, "0x%02X ", (int)(c & 0xFF)); - output.append(buff, amt); - } - return output; -} - -// todo!("already ported, delete this"); -/// Test wide/narrow conversion by creating random strings and verifying that the original string -/// comes back through double conversion. -static void test_convert() { - say(L"Testing wide/narrow string conversion"); - for (int i = 0; i < ESCAPE_TEST_COUNT; i++) { - std::string orig{}; - while (random() % ESCAPE_TEST_LENGTH) { - char c = random(); - orig.push_back(c); - } - - const wcstring w = str2wcstring(orig); - std::string n = wcs2string(w); - if (orig != n) { - err(L"Line %d - %d: Conversion cycle of string:\n%4d chars: %s\n" - L"produced different string:\n%4d chars: %s", - __LINE__, i, orig.size(), str2hex(orig).c_str(), n.size(), str2hex(n).c_str()); - } - } -} - -// todo!("already ported, delete this"); -/// Verify that ASCII narrow->wide conversions are correct. -static void test_convert_ascii() { - std::string s(4096, '\0'); - for (size_t i = 0; i < s.size(); i++) { - s[i] = (i % 10) + '0'; - } - - // Test a variety of alignments. - for (size_t left = 0; left < 16; left++) { - for (size_t right = 0; right < 16; right++) { - const char *start = s.data() + left; - size_t len = s.size() - left - right; - wcstring wide = str2wcstring(start, len); - std::string narrow = wcs2string(wide); - do_test(narrow == std::string(start, len)); - } - } - - // Put some non-ASCII bytes in and ensure it all still works. - for (char &c : s) { - char saved = c; - c = 0xF7; - do_test(wcs2string(str2wcstring(s)) == s); - c = saved; - } -} - -// todo!("already ported, delete this"); -/// fish uses the private-use range to encode bytes that could not be decoded using the user's -/// locale. If the input could be decoded, but decoded to private-use codepoints, then fish should -/// also use the direct encoding for those bytes. Verify that characters in the private use area are -/// correctly round-tripped. See #7723. -static void test_convert_private_use() { - for (wchar_t wc = ENCODE_DIRECT_BASE; wc < ENCODE_DIRECT_END; wc++) { - // Encode the char via the locale. Do not use fish functions which interpret these - // specially. - char converted[MB_LEN_MAX]; - mbstate_t state{}; - size_t len = std::wcrtomb(converted, wc, &state); - if (len == static_cast(-1)) { - // Could not be encoded in this locale. - continue; - } - std::string s(converted, len); - - // Ask fish to decode this via str2wcstring. - // str2wcstring should notice that the decoded form collides with its private use and encode - // it directly. - wcstring ws = str2wcstring(s); - - // Each byte should be encoded directly, and round tripping should work. - do_test(ws.size() == s.size()); - do_test(wcs2string(ws) == s); - } -} - -// todo!("already ported, delete this"); -static void test_iothread() { - say(L"Testing iothreads"); - std::atomic shared_int{0}; - const int iterations = 64; - std::promise prom; - for (int i = 0; i < iterations; i++) { - iothread_perform([&] { - int newv = 1 + shared_int.fetch_add(1, std::memory_order_relaxed); - if (newv == iterations) { - prom.set_value(); - } - }); - } - auto status = prom.get_future().wait_for(std::chrono::seconds(64)); - - // Should have incremented it once per thread. - do_test(status == std::future_status::ready); - do_test(shared_int == iterations); - if (shared_int != iterations) { - say(L"Expected int to be %d, but instead it was %d", iterations, shared_int.load()); - } -} - -static void test_const_strlen() { - do_test(const_strlen("") == 0); - do_test(const_strlen(L"") == 0); - do_test(const_strlen("\0") == 0); - do_test(const_strlen(L"\0") == 0); - do_test(const_strlen("\0abc") == 0); - do_test(const_strlen(L"\0abc") == 0); - do_test(const_strlen("x") == 1); - do_test(const_strlen(L"x") == 1); - do_test(const_strlen("abc") == 3); - do_test(const_strlen(L"abc") == 3); - do_test(const_strlen("abc\0def") == 3); - do_test(const_strlen(L"abc\0def") == 3); - do_test(const_strlen("abcdef\0") == 6); - do_test(const_strlen(L"abcdef\0") == 6); - static_assert(const_strlen("hello") == 5, "const_strlen failure"); -} - -static void test_const_strcmp() { - static_assert(const_strcmp("", "a") < 0, "const_strcmp failure"); - static_assert(const_strcmp("a", "a") == 0, "const_strcmp failure"); - static_assert(const_strcmp("a", "") > 0, "const_strcmp failure"); - static_assert(const_strcmp("aa", "a") > 0, "const_strcmp failure"); - static_assert(const_strcmp("a", "aa") < 0, "const_strcmp failure"); - static_assert(const_strcmp("b", "aa") > 0, "const_strcmp failure"); -} - -// todo!("already ported, delete this"); -void test_dir_iter() { - dir_iter_t baditer(L"/definitely/not/a/valid/directory/for/sure"); - do_test(!baditer.valid()); - do_test(baditer.error() == ENOENT || baditer.error() == EACCES); - do_test(baditer.next() == nullptr); - - char t1[] = "/tmp/fish_test_dir_iter.XXXXXX"; - const std::string basepathn = mkdtemp(t1); - const wcstring basepath = str2wcstring(basepathn); - auto makepath = [&](const wcstring &s) { return wcs2zstring(basepath + L"/" + s); }; - - const wcstring dirname = L"dir"; - const wcstring regname = L"reg"; - const wcstring reglinkname = L"reglink"; // link to regular file - const wcstring dirlinkname = L"dirlink"; // link to directory - const wcstring badlinkname = L"badlink"; // link to nowhere - const wcstring selflinkname = L"selflink"; // link to self - const wcstring fifoname = L"fifo"; - const std::vector names = {dirname, regname, reglinkname, dirlinkname, - badlinkname, selflinkname, fifoname}; - - const auto is_link_name = [&](const wcstring &name) -> bool { - return contains({reglinkname, dirlinkname, badlinkname, selflinkname}, name); - }; - - // Make our different file types - int ret = mkdir(makepath(dirname).c_str(), 0700); - do_test(ret == 0); - ret = open(makepath(regname).c_str(), O_CREAT | O_WRONLY, 0600); - do_test(ret >= 0); - close(ret); - ret = symlink(makepath(regname).c_str(), makepath(reglinkname).c_str()); - do_test(ret == 0); - ret = symlink(makepath(dirname).c_str(), makepath(dirlinkname).c_str()); - do_test(ret == 0); - ret = symlink("/this/is/an/invalid/path", makepath(badlinkname).c_str()); - do_test(ret == 0); - ret = symlink(makepath(selflinkname).c_str(), makepath(selflinkname).c_str()); - do_test(ret == 0); - ret = mkfifo(makepath(fifoname).c_str(), 0600); - do_test(ret == 0); - - dir_iter_t iter1(basepath); - do_test(iter1.valid()); - do_test(iter1.error() == 0); - size_t seen = 0; - while (const auto *entry = iter1.next()) { - seen += 1; - do_test(entry->name != L"." && entry->name != L".."); - do_test(contains(names, entry->name)); - maybe_t expected{}; - if (entry->name == dirname) { - expected = dir_entry_type_t::dir; - } else if (entry->name == regname) { - expected = dir_entry_type_t::reg; - } else if (entry->name == reglinkname) { - expected = dir_entry_type_t::reg; - } else if (entry->name == dirlinkname) { - expected = dir_entry_type_t::dir; - } else if (entry->name == badlinkname) { - expected = none(); - } else if (entry->name == selflinkname) { - expected = dir_entry_type_t::lnk; - } else if (entry->name == fifoname) { - expected = dir_entry_type_t::fifo; - } else { - err(L"Unexpected file type"); - continue; - } - // Links should never have a fast type if we are resolving them, since we cannot resolve a - // symlink from readdir. - if (is_link_name(entry->name)) { - do_test(entry->fast_type() == none()); - } - // If we have a fast type, it should be correct. - do_test(entry->fast_type() == none() || entry->fast_type() == expected); - do_test(entry->check_type() == expected); - } - do_test(seen == names.size()); - - // Clean up. - for (const auto &name : names) { - (void)unlink(makepath(name).c_str()); - } - (void)rmdir(basepathn.c_str()); -} - -static void test_utility_functions() { - say(L"Testing utility functions"); - test_const_strlen(); - test_const_strcmp(); -} - -class test_lru_t : public lru_cache_t { - public: - static constexpr size_t test_capacity = 16; - using value_type = std::pair; - - test_lru_t() : lru_cache_t(test_capacity) {} - - std::vector values() const { - std::vector result; - for (auto p : *this) { - result.push_back(p); - } - return result; - } - - std::vector ints() const { - std::vector result; - for (auto p : *this) { - result.push_back(p.second); - } - return result; - } -}; - -// todo!("no need to port this, delete this"); -static void test_lru() { - say(L"Testing LRU cache"); - - test_lru_t cache; - std::vector> expected_evicted; - std::vector> expected_values; - int total_nodes = 20; - for (int i = 0; i < total_nodes; i++) { - do_test(cache.size() == size_t(std::min(i, 16))); - do_test(cache.values() == expected_values); - if (i < 4) expected_evicted.emplace_back(to_string(i), i); - // Adding the node the first time should work, and subsequent times should fail. - do_test(cache.insert(to_string(i), i)); - do_test(!cache.insert(to_string(i), i + 1)); - - expected_values.emplace_back(to_string(i), i); - while (expected_values.size() > test_lru_t::test_capacity) { - expected_values.erase(expected_values.begin()); - } - cache.check_sanity(); - } - do_test(cache.values() == expected_values); - cache.check_sanity(); - - // Stable-sort ints in reverse order - // This a/2 check ensures that some different ints compare the same - // It also gives us a different order than we started with - auto comparer = [](int a, int b) { return a / 2 > b / 2; }; - std::vector ints = cache.ints(); - std::stable_sort(ints.begin(), ints.end(), comparer); - - cache.stable_sort(comparer); - std::vector new_ints = cache.ints(); - if (new_ints != ints) { - auto commajoin = [](const std::vector &vs) { - wcstring ret; - for (int v : vs) { - append_format(ret, L"%d,", v); - } - if (!ret.empty()) ret.pop_back(); - return ret; - }; - err(L"LRU stable sort failed. Expected %ls, got %ls\n", commajoin(new_ints).c_str(), - commajoin(ints).c_str()); - } - - cache.evict_all_nodes(); - do_test(cache.size() == 0); -} - -// todo!("already ported, delete this") -/// Testing colors. -static void test_colors() { - say(L"Testing colors"); - do_test(rgb_color_t(L"#FF00A0").is_rgb()); - do_test(rgb_color_t(L"FF00A0").is_rgb()); - do_test(rgb_color_t(L"#F30").is_rgb()); - do_test(rgb_color_t(L"F30").is_rgb()); - do_test(rgb_color_t(L"f30").is_rgb()); - do_test(rgb_color_t(L"#FF30a5").is_rgb()); - do_test(rgb_color_t(L"3f30").is_none()); - do_test(rgb_color_t(L"##f30").is_none()); - do_test(rgb_color_t(L"magenta").is_named()); - do_test(rgb_color_t(L"MaGeNTa").is_named()); - do_test(rgb_color_t(L"mooganta").is_none()); -} - -// todo!("port this") -/// Helper for test_timezone_env_vars(). -long return_timezone_hour(time_t tstamp, const wchar_t *timezone) { - env_stack_t vars{parser_principal_parser()->deref().vars_boxed()}; - struct tm ltime; - char ltime_str[3]; - char *str_ptr; - size_t n; - - vars.set_one(L"TZ", ENV_EXPORT, timezone); - - const auto var = vars.get(L"TZ", ENV_DEFAULT); - (void)var; - - localtime_r(&tstamp, <ime); - n = strftime(ltime_str, 3, "%H", <ime); - if (n != 2) { - err(L"strftime() returned %d, expected 2", n); - return 0; - } - return strtol(ltime_str, &str_ptr, 10); -} - -// todo!("no need to port, delete this") -void test_maybe() { - say(L"Testing maybe_t"); - // maybe_t bool conversion is only enabled for non-bool-convertible T types - do_test(!bool(maybe_t())); - maybe_t m(3); - do_test(m.has_value()); - do_test(m.value() == 3); - m.reset(); - do_test(!m.has_value()); - m = 123; - do_test(m.has_value()); - do_test(m.has_value() && m.value() == 123); - m = maybe_t(); - do_test(!m.has_value()); - m = maybe_t(64); - do_test(m.has_value() && m.value() == 64); - m = 123; - do_test(m == maybe_t(123)); - do_test(m != maybe_t()); - do_test(maybe_t() == none()); - do_test(!maybe_t(none()).has_value()); - maybe_t n = none(); - do_test(!bool(n)); - - maybe_t m0 = none(); - maybe_t m3("hi"); - maybe_t m4 = m3; - do_test(m4 && *m4 == "hi"); - maybe_t m5 = m0; - do_test(!m5); - - maybe_t acquire_test("def"); - do_test(acquire_test); - std::string res = acquire_test.acquire(); - do_test(!acquire_test); - do_test(res == "def"); - - // maybe_t should be copyable iff T is copyable. - using copyable = std::shared_ptr; - using noncopyable = std::unique_ptr; - do_test(std::is_copy_assignable>::value == true); - do_test(std::is_copy_constructible>::value == true); - do_test(std::is_copy_assignable>::value == false); - do_test(std::is_copy_constructible>::value == false); - - // We can construct a maybe_t from noncopyable things. - maybe_t nmt{make_unique(42)}; - do_test(**nmt == 42); - - maybe_t c1{"abc"}; - maybe_t c2 = c1; - do_test(c1.value() == "abc"); - do_test(c2.value() == "abc"); - c2 = c1; - do_test(c1.value() == "abc"); - do_test(c2.value() == "abc"); - - do_test(c2.value_or("derp") == "abc"); - do_test(c2.value_or("derp") == "abc"); - c2.reset(); - do_test(c2.value_or("derp") == "derp"); -} - -// todo!("already ported, delete this") -void test_normalize_path() { - say(L"Testing path normalization"); - do_test(normalize_path(L"") == L"."); - do_test(normalize_path(L"..") == L".."); - do_test(normalize_path(L"./") == L"."); - do_test(normalize_path(L"./.") == L"."); - do_test(normalize_path(L"/") == L"/"); - do_test(normalize_path(L"//") == L"//"); - do_test(normalize_path(L"///") == L"/"); - do_test(normalize_path(L"////") == L"/"); - do_test(normalize_path(L"/.///") == L"/"); - do_test(normalize_path(L".//") == L"."); - do_test(normalize_path(L"/.//../") == L"/"); - do_test(normalize_path(L"////abc") == L"/abc"); - do_test(normalize_path(L"/abc") == L"/abc"); - do_test(normalize_path(L"/abc/") == L"/abc"); - do_test(normalize_path(L"/abc/..def/") == L"/abc/..def"); - do_test(normalize_path(L"//abc/../def/") == L"//def"); - do_test(normalize_path(L"abc/../abc/../abc/../abc") == L"abc"); - do_test(normalize_path(L"../../") == L"../.."); - do_test(normalize_path(L"foo/./bar") == L"foo/bar"); - do_test(normalize_path(L"foo/../") == L"."); - do_test(normalize_path(L"foo/../foo") == L"foo"); - do_test(normalize_path(L"foo/../foo/") == L"foo"); - do_test(normalize_path(L"foo/././bar/.././baz") == L"foo/baz"); - - do_test(path_normalize_for_cd(L"/", L"..") == L"/.."); - do_test(path_normalize_for_cd(L"/abc/", L"..") == L"/"); - do_test(path_normalize_for_cd(L"/abc/def/", L"..") == L"/abc"); - do_test(path_normalize_for_cd(L"/abc/def/", L"../..") == L"/"); - do_test(path_normalize_for_cd(L"/abc///def/", L"../..") == L"/"); - do_test(path_normalize_for_cd(L"/abc///def/", L"../..") == L"/"); - do_test(path_normalize_for_cd(L"/abc///def///", L"../..") == L"/"); - do_test(path_normalize_for_cd(L"/abc///def///", L"..") == L"/abc"); - do_test(path_normalize_for_cd(L"/abc///def///", L"..") == L"/abc"); - do_test(path_normalize_for_cd(L"/abc/def/", L"./././/..") == L"/abc"); - do_test(path_normalize_for_cd(L"/abc/def/", L"../../../") == L"/../"); - do_test(path_normalize_for_cd(L"/abc/def/", L"../ghi/..") == L"/abc/ghi/.."); -} - -// todo!("already ported, delete this") -void test_dirname_basename() { - say(L"Testing wdirname and wbasename"); - const struct testcase_t { - const wchar_t *path; - const wchar_t *dir; - const wchar_t *base; - } testcases[] = { - {L"", L".", L"."}, - {L"foo//", L".", L"foo"}, - {L"foo//////", L".", L"foo"}, - {L"/////foo", L"/", L"foo"}, - {L"/////foo", L"/", L"foo"}, - {L"//foo/////bar", L"//foo", L"bar"}, - {L"foo/////bar", L"foo", L"bar"}, - - // Examples given in XPG4.2. - {L"/usr/lib", L"/usr", L"lib"}, - {L"usr", L".", L"usr"}, - {L"/", L"/", L"/"}, - {L".", L".", L"."}, - {L"..", L".", L".."}, - }; - for (const auto &tc : testcases) { - wcstring dir = wdirname(tc.path); - if (dir != tc.dir) { - err(L"Wrong dirname for \"%ls\": expected \"%ls\", got \"%ls\"", tc.path, tc.dir, - dir.c_str()); - } - wcstring base = wbasename(tc.path); - if (base != tc.base) { - err(L"Wrong basename for \"%ls\": expected \"%ls\", got \"%ls\"", tc.path, tc.base, - base.c_str()); - } - } - // Ensures strings which greatly exceed PATH_MAX still work (#7837). - wcstring longpath = get_overlong_path(); - wcstring longpath_dir = longpath.substr(0, longpath.rfind(L'/')); - do_test(wdirname(longpath) == longpath_dir); - do_test(wbasename(longpath) == L"overlong"); -} - -// typedef void (test_entry_point_t)(); -using test_entry_point_t = void (*)(); -struct test_t { - const char *group; - std::function test; - bool opt_in = false; - - test_t(const char *group, test_entry_point_t test, bool opt_in = false) - : group(group), test(test), opt_in(opt_in) {} -}; - -struct test_comparator_t { - // template - int operator()(const test_t &lhs, const test_t &rhs) { return strcmp(lhs.group, rhs.group); } -}; - -// This magic string is required for CMake to pick up the list of tests -#define TEST_GROUP(x) x -static const test_t s_tests[]{ - {TEST_GROUP("utility_functions"), test_utility_functions}, - {TEST_GROUP("dir_iter"), test_dir_iter}, - {TEST_GROUP("str_to_num"), test_str_to_num}, - {TEST_GROUP("enum"), test_enum_set}, - {TEST_GROUP("enum"), test_enum_array}, - {TEST_GROUP("convert"), test_convert}, - {TEST_GROUP("convert"), test_convert_private_use}, - {TEST_GROUP("convert_ascii"), test_convert_ascii}, - {TEST_GROUP("iothread"), test_iothread}, - {TEST_GROUP("lru"), test_lru}, - {TEST_GROUP("colors"), test_colors}, - {TEST_GROUP("maybe"), test_maybe}, - {TEST_GROUP("normalize"), test_normalize_path}, - {TEST_GROUP("dirname"), test_dirname_basename}, -}; - -void list_tests() { - std::set groups; - for (const auto &test : s_tests) { - groups.insert(test.group); - } - - for (const auto &group : groups) { - std::fprintf(stdout, "%s\n", group.c_str()); - } -} - -/// Main test. -int main(int argc, char **argv) { - std::setlocale(LC_ALL, ""); - - if (argc >= 2 && std::strcmp(argv[1], "--list") == 0) { - list_tests(); - return 0; - } - - // Look for the file tests/test.fish. We expect to run in a directory containing that file. - // If we don't find it, walk up the directory hierarchy until we do, or error. - while (access("./tests/test.fish", F_OK) != 0) { - char wd[PATH_MAX + 1] = {}; - if (!getcwd(wd, sizeof wd)) { - perror("getcwd"); - exit(-1); - } - if (!std::strcmp(wd, "/")) { - std::fwprintf( - stderr, L"Unable to find 'tests' directory, which should contain file test.fish\n"); - exit(EXIT_FAILURE); - } - if (chdir(dirname(wd)) < 0) { - perror("chdir"); - } - } - - srandom((unsigned int)time(nullptr)); - configure_thread_assertions_for_testing(); - - // Set the program name to this sentinel value - // This will prevent some misleading stderr output during the tests - program_name = TESTS_PROGRAM_NAME; - s_arguments = argv + 1; - - struct utsname uname_info; - uname(&uname_info); - - say(L"Testing low-level functionality"); - rust_init(); - proc_init(); - rust_env_init(true); - misc_init(); - reader_init(); - - // Set default signal handlers, so we can ctrl-C out of this. - signal_reset_handlers(); - - // Set PWD from getcwd - fixes #5599 - env_stack_principal().set_pwd_from_getcwd(); - - for (const auto &test : s_tests) { - if (should_test_function(test.group)) { - test.test(); - } - } - - say(L"Encountered %d errors in low-level tests", err_count); - if (s_test_run_count == 0) say(L"*** No Tests Were Actually Run! ***"); - - // If under ASAN, reclaim some resources before exiting. - asan_before_exit(); - - if (err_count != 0) { - return 1; - } -}