ATF_TC_BODY(cleanup__mount_point__busy, tc) { ATF_REQUIRE(mkdir("root", 0755) != -1); ATF_REQUIRE(mkdir("root/dir1", 0755) != -1); mount_tmpfs("root/dir1"); pid_t pid = fork(); ATF_REQUIRE(pid != -1); if (pid == 0) { if (chdir("root/dir1") == -1) abort(); atf_utils_create_file("dont-delete-me", "%s", ""); atf_utils_create_file("../../done", "%s", ""); pause(); exit(EXIT_SUCCESS); } else { fprintf(stderr, "Waiting for child to finish preparations\n"); while (!atf_utils_file_exists("done")) {} fprintf(stderr, "Child done; cleaning up\n"); ATF_REQUIRE(kyua_error_is_set(kyua_fs_cleanup("root"))); ATF_REQUIRE(atf_utils_file_exists("root/dir1/dont-delete-me")); fprintf(stderr, "Killing child\n"); ATF_REQUIRE(kill(pid, SIGKILL) != -1); int status; ATF_REQUIRE(waitpid(pid, &status, 0) != -1); ATF_REQUIRE(!kyua_error_is_set(kyua_fs_cleanup("root"))); ATF_REQUIRE(!lookup(".", "root", DT_DIR)); } }
/// Parses a single test case and writes it to the output. /// /// This has to be called after the ident property has been read, and takes care /// of reading the rest of the test case and printing the parsed result. /// /// Be aware that this consumes the newline after the test case. The caller /// should not look for it. /// /// \param [in,out] input File from which to read the header. /// \param [in,out] output File to which to write the parsed test case. /// \param [in,out] name The name of the test case. This is a non-const pointer /// and the input string is modified to simplify tokenization. /// /// \return OK if the parsing succeeds; an error otherwise. static kyua_error_t parse_test_case(FILE* input, FILE* output, char* name) { kyua_error_t error; char line[1024]; // It's ugly to have a limit, but it's easier this way. fprintf(output, "test_case{name="); print_quoted(name, output, true); error = kyua_error_ok(); while (!kyua_error_is_set(error) && fgets_no_newline(line, sizeof(line), input) != NULL && strcmp(line, "") != 0) { char* key = NULL; char* value = NULL; error = parse_property(line, &key, &value); if (!kyua_error_is_set(error)) { const char* out_key = rewrite_property(key); if (out_key == rewrite_error) { error = kyua_generic_error_new("Unknown ATF property %s", key); } else if (out_key == NULL) { fprintf(output, ", ['custom."); print_quoted(key, output, false); fprintf(output, "']="); print_quoted(value, output, true); } else { fprintf(output, ", %s=", out_key); print_quoted(value, output, true); } } } fprintf(output, "}\n"); return error; }
/// Sets a collection of configuration variables in the environment. /// /// \param user_variables Set of configuration variables to pass to the test. /// This is an array of strings of the form var=value and must have been /// previously sanity-checked with kyua_env_check_configuration. /// /// \return An error if there is a problem allocating memory. /// /// \post The environment contains a new collection of TEST_ENV_* variables that /// matches the input user_variables. kyua_error_t kyua_env_set_configuration(const char* const user_variables[]) { const char* const* iter; for (iter = user_variables; *iter != NULL; ++iter) { kyua_error_t error; char* var_value = strdup(*iter); if (var_value == NULL) return kyua_oom_error_new(); char* value = strchr(var_value, '='); assert(value != NULL); // Must have been validated. *value = '\0'; value += 1; char* var; error = kyua_text_printf(&var, "TEST_ENV_%s", var_value); if (kyua_error_is_set(error)) { free(var_value); return error; } error = kyua_env_set(var, value); if (kyua_error_is_set(error)) { free(var_value); return error; } } return kyua_error_ok(); }
ATF_TC_BODY(fork_wait__core_size, tc) { struct rlimit rl; rl.rlim_cur = 0; rl.rlim_max = RLIM_INFINITY; if (setrlimit(RLIMIT_CORE, &rl) == -1) atf_tc_skip("Failed to lower the core size limit"); kyua_run_params_t run_params; kyua_run_params_init(&run_params); pid_t pid; kyua_error_t error = kyua_run_fork(&run_params, &pid); if (!kyua_error_is_set(error) && pid == 0) abort(); ATF_REQUIRE(!kyua_error_is_set(error)); int status; bool timed_out; error = kyua_run_wait(pid, &status, &timed_out); if (kyua_error_is_set(error)) atf_tc_fail("wait failed; unexpected problem during exec?"); ATF_REQUIRE(!timed_out); ATF_REQUIRE(WIFSIGNALED(status)); ATF_REQUIRE_MSG(WCOREDUMP(status), "Core not dumped as expected"); }
/// Rewrites the test cases list from the input to the output. /// /// \param [in,out] input Stream from which to read the test program's test /// cases list. The current location must be after the header and at the /// first identifier (if any). /// \param [out] output Stream to which to write the generic list. /// /// \return An error object. static kyua_error_t parse_tests(FILE* input, FILE* output) { char line[512]; // It's ugly to have a limit, but it's easier this way. if (fgets_no_newline(line, sizeof(line), input) == NULL) { return fgets_error(input, "Empty test cases list"); } kyua_error_t error; do { char* key = NULL; char* value = NULL; error = parse_property(line, &key, &value); if (kyua_error_is_set(error)) break; if (strcmp(key, "ident") == 0) { error = parse_test_case(input, output, value); } else { error = kyua_generic_error_new("Expected ident property, got %s", key); } } while (!kyua_error_is_set(error) && fgets_no_newline(line, sizeof(line), input) != NULL); if (!kyua_error_is_set(error)) { if (ferror(input)) error = kyua_libc_error_new(errno, "fgets failed"); else assert(feof(input)); } return error; }
/// Body of a subprocess to execute GDB. /// /// This should be called from the child created by a kyua_run_fork() call, /// which means that we do not have to take care of isolating the process. /// /// \pre The caller must have flushed stdout before spawning this process, to /// prevent double-flushing and/or corruption of data. /// /// \param program Path to the program being debugged. Can be relative to /// the given work directory. /// \param core_name Path to the dumped core. Use find_core() to deduce /// a valid candidate. Can be relative to the given work directory. /// \param output Stream to which to send the output of GDB. static void run_gdb(const char* program, const char* core_name, FILE* output) { // TODO(jmmv): Should be done by kyua_run_fork(), but doing so would change // the semantics of the ATF interface. Need to evaluate this carefully. const kyua_error_t error = kyua_env_unset("TERM"); if (kyua_error_is_set(error)) { kyua_error_warn(error, "Failed to unset TERM; GDB may misbehave"); free(error); } (void)close(STDIN_FILENO); #if defined(__minix) && !defined(NDEBUG) const int input_fd = #endif /* defined(__minix) && !defined(NDEBUG) */ open("/dev/null", O_RDONLY); assert(input_fd == STDIN_FILENO); const int output_fd = fileno(output); assert(output_fd != -1); // We expect a file-backed stream. if (output_fd != STDOUT_FILENO) { fflush(stdout); (void)dup2(output_fd, STDOUT_FILENO); } if (output_fd != STDERR_FILENO) { fflush(stderr); (void)dup2(output_fd, STDERR_FILENO); } if (output_fd != STDOUT_FILENO && output_fd != STDERR_FILENO) fclose(output); const char* const gdb_args[] = { "gdb", "-batch", "-q", "-ex", "bt", program, core_name, NULL }; kyua_run_exec(kyua_stacktrace_gdb, gdb_args); }
/// Executes the cleanup of a test case. /// /// \param test_program Path to the test program to execute. /// \param test_case Name of the test case to run. /// \param user_variables Set of configuration variables to pass to the test. static void exec_cleanup(const char* test_program, const char* test_case, const char* const user_variables[]) { char* name; kyua_error_t error = kyua_text_printf(&name, "%s:cleanup", test_case); if (kyua_error_is_set(error)) kyua_error_err(EXIT_FAILURE, error, "Failed to construct argument list"); const size_t nargs = 1 /* test_program */ + + 2 * count_variables(user_variables) /* -v name=value */ + 1 /* test_case */ + 1 /* NULL */; const char** args = malloc(sizeof(const char*) * nargs); if (args == NULL) kyua_error_err(EXIT_FAILURE, kyua_oom_error_new(), "Failed to construct arguments list"); size_t i = 0; args[i++] = test_program; const char* const* iter; for (iter = user_variables; *iter != NULL; ++iter) { args[i++] = "-v"; args[i++] = *iter; } args[i++] = name; args[i++] = NULL; assert(i == nargs); kyua_run_exec(test_program, args); }
/// Runs a single test cases of a test program. /// /// \param test_program Path to the test program for which to list the test /// cases. Should be absolute. /// \param test_case Name of the test case to run. /// \param result_file Path to the file to which to write the result of the /// test. Should be absolute. /// \param user_variables Array of name=value pairs that describe the user /// configuration variables for the test case. /// \param run_params Execution parameters to configure the test process. /// \param [out] success Set to true if the test case reported a valid exit /// condition (like "passed" or "skipped"); false otherwise. This is /// only updated if the method returns OK. /// /// \return An error if the listing fails; OK otherwise. static kyua_error_t run_test_case(const char* test_program, const char* test_case, const char* result_file, const char* const user_variables[], const kyua_run_params_t* run_params, bool* success) { kyua_error_t error; char* work_directory; error = kyua_run_work_directory_enter(WORKDIR_TEMPLATE, run_params->unprivileged_user, run_params->unprivileged_group, &work_directory); if (kyua_error_is_set(error)) goto out; kyua_run_params_t real_run_params = *run_params; real_run_params.work_directory = work_directory; error = run_body(test_program, test_case, result_file, user_variables, &real_run_params, success); if (has_cleanup(user_variables)) { error = run_cleanup(test_program, test_case, result_file, user_variables, &real_run_params, *success, success); } error = kyua_error_subsume(error, kyua_run_work_directory_leave(&work_directory)); out: return error; }
ATF_TC_BODY(cleanup__subdir__empty, tc) { ATF_REQUIRE(mkdir("root", 0755) != -1); ATF_REQUIRE(lookup(".", "root", DT_DIR)); ATF_REQUIRE(!kyua_error_is_set(kyua_fs_cleanup("root"))); ATF_REQUIRE(!lookup(".", "root", DT_DIR)); }
ATF_TC_BODY(cleanup__file, tc) { atf_utils_create_file("root", "%s", ""); ATF_REQUIRE(lookup(".", "root", DT_REG)); ATF_REQUIRE(!kyua_error_is_set(kyua_fs_cleanup("root"))); ATF_REQUIRE(!lookup(".", "root", DT_REG)); }
ATF_TC_BODY(vprintf__empty, tc) { char* buffer; kyua_error_t error = call_vprintf(&buffer, "%s", ""); ATF_REQUIRE(!kyua_error_is_set(error)); ATF_REQUIRE_STREQ("", buffer); }
ATF_TC_BODY(concat__two, tc) { char* path; ATF_REQUIRE(!kyua_error_is_set(kyua_fs_concat(&path, "foo", "bar", NULL))); ATF_REQUIRE_STREQ("foo/bar", path); free(path); }
ATF_TC_BODY(vprintf__some, tc) { char* buffer; kyua_error_t error = call_vprintf(&buffer, "this is %d %s", 123, "foo"); ATF_REQUIRE(!kyua_error_is_set(error)); ATF_REQUIRE_STREQ("this is 123 foo", buffer); free(buffer); }
ATF_TC_BODY(make_absolute__absolute, tc) { char* absolute; ATF_REQUIRE(!kyua_error_is_set(kyua_fs_make_absolute( "/this/is/absolute", &absolute))); ATF_REQUIRE_STREQ("/this/is/absolute", absolute); free(absolute); }
ATF_TC_BODY(fork_exec_wait__ok, tc) { const char* const args[] = {"sh", "-c", "exit 42", NULL}; int exitstatus = -1; // Shut up GCC warning. const kyua_error_t error = exec_check("/bin/sh", args, &exitstatus); ATF_REQUIRE(!kyua_error_is_set(error)); ATF_REQUIRE_EQ(42, exitstatus); }
ATF_TC_BODY(concat__several, tc) { char* path; ATF_REQUIRE(!kyua_error_is_set(kyua_fs_concat(&path, "/usr", ".", "bin", "ls", NULL))); ATF_REQUIRE_STREQ("/usr/./bin/ls", path); free(path); }
/// Parses a results file written by an ATF test case. /// /// \param input_name Path to the result file to parse. /// \param [out] status Type of result. /// \param [out] status_arg Optional integral argument to the status. /// \param [out] reason Textual explanation of the result, if any. /// \param reason_size Length of the reason output buffer. /// /// \return An error if the input_name file has an invalid syntax; OK otherwise. static kyua_error_t read_atf_result(const char* input_name, enum atf_status* status, int* status_arg, char* const reason, const size_t reason_size) { kyua_error_t error = kyua_error_ok(); FILE* input = fopen(input_name, "r"); if (input == NULL) { error = kyua_generic_error_new("Premature exit"); goto out; } char line[1024]; if (fgets(line, sizeof(line), input) == NULL) { if (ferror(input)) { error = kyua_libc_error_new(errno, "Failed to read result from " "file %s", input_name); goto out_input; } else { assert(feof(input)); error = kyua_generic_error_new("Empty result file %s", input_name); goto out_input; } } if (!trim_newline(line)) { error = kyua_generic_error_new("Missing newline in result file"); goto out_input; } char* reason_start = strstr(line, ": "); if (reason_start != NULL) { *reason_start = '\0'; *(reason_start + 1) = '\0'; reason_start += 2; } bool need_reason = false; // Initialize to shut up gcc warning. error = parse_status(line, status, status_arg, &need_reason); if (kyua_error_is_set(error)) goto out_input; if (need_reason) { error = read_reason(input, reason_start, reason, reason_size); } else { if (reason_start != NULL || !is_really_eof(input)) { error = kyua_generic_error_new("Found unexpected reason in passed " "test result"); goto out_input; } reason[0] = '\0'; } out_input: fclose(input); out: return error; }
ATF_TC_BODY(cleanup__subdir__unprotect_symlink, tc) { ATF_REQUIRE(mkdir("root", 0755) != -1); ATF_REQUIRE(mkdir("root/dir1", 0755) != -1); ATF_REQUIRE(symlink("/bin/ls", "root/dir1/ls") != -1); ATF_REQUIRE(chmod("root/dir1", 0555) != -1); ATF_REQUIRE(!kyua_error_is_set(kyua_fs_cleanup("root"))); ATF_REQUIRE(!lookup(".", "root", DT_DIR)); }
ATF_TC_BODY(cleanup__mount_point__simple, tc) { ATF_REQUIRE(mkdir("root", 0755) != -1); ATF_REQUIRE(mkdir("root/dir1", 0755) != -1); atf_utils_create_file("root/zz", "%s", ""); mount_tmpfs("root/dir1"); ATF_REQUIRE(!kyua_error_is_set(kyua_fs_cleanup("root"))); ATF_REQUIRE(!lookup(".", "root", DT_DIR)); }
ATF_TC_BODY(sanitize__fail, tc) { char* sane; kyua_error_t error = kyua_fs_sanitize("non-existent/path", &sane); ATF_REQUIRE(kyua_error_is_set(error)); ATF_REQUIRE(kyua_error_is_type(error, "libc")); ATF_REQUIRE_EQ(ENOENT, kyua_libc_error_errno(error)); kyua_error_free(error); }
ATF_TC_BODY(current_path__enoent, tc) { char* previous; ATF_REQUIRE(!kyua_error_is_set(kyua_fs_current_path(&previous))); ATF_REQUIRE(mkdir("root", 0755) != -1); ATF_REQUIRE(chdir("root") != -1); ATF_REQUIRE(rmdir("../root") != -1); char* cwd = (char*)0xdeadbeef; kyua_error_t error = kyua_fs_current_path(&cwd); ATF_REQUIRE(kyua_error_is_set(error)); ATF_REQUIRE(kyua_error_is_type(error, "libc")); ATF_REQUIRE_EQ(ENOENT, kyua_libc_error_errno(error)); ATF_REQUIRE_EQ((char*)0xdeadbeef, cwd); kyua_error_free(error); free(previous); }
ATF_TC_BODY(fork_exec_wait__enoent, tc) { const char* const args[] = {"foo", NULL}; int unused_exitstatus; const kyua_error_t error = exec_check("./foo", args, &unused_exitstatus); ATF_REQUIRE(kyua_error_is_set(error)); ATF_REQUIRE(kyua_error_is_type(error, "libc")); ATF_REQUIRE_EQ(ENOENT, kyua_libc_error_errno(error)); }
ATF_TC_BODY(cleanup__mount_point__links, tc) { ATF_REQUIRE(mkdir("root", 0755) != -1); ATF_REQUIRE(mkdir("root/dir1", 0755) != -1); ATF_REQUIRE(mkdir("root/dir3", 0755) != -1); mount_tmpfs("root/dir1"); ATF_REQUIRE(symlink("../dir3", "root/dir1/link") != -1); ATF_REQUIRE(!kyua_error_is_set(kyua_fs_cleanup("root"))); ATF_REQUIRE(!lookup(".", "root", DT_DIR)); }
ATF_TC_BODY(sanitize__ok, tc) { ATF_REQUIRE(mkdir("a", 0755) != -1); ATF_REQUIRE(mkdir("a/bc", 0755) != -1); ATF_REQUIRE(mkdir("a/bc/12-34", 0755) != -1); char* sane; ATF_REQUIRE(!kyua_error_is_set(kyua_fs_sanitize( ".//a/bc///12-34", &sane))); char *expected; ATF_REQUIRE(!kyua_error_is_set(kyua_fs_make_absolute( "a/bc/12-34", &expected))); ATF_REQUIRE_STREQ(expected, sane); free(sane); free(expected); }
ATF_TC_BODY(current_path__ok, tc) { char* previous; ATF_REQUIRE(!kyua_error_is_set(kyua_fs_current_path(&previous))); ATF_REQUIRE(mkdir("root", 0755) != -1); ATF_REQUIRE(chdir("root") != -1); char* cwd; ATF_REQUIRE(!kyua_error_is_set(kyua_fs_current_path(&cwd))); char* exp_cwd; ATF_REQUIRE(!kyua_error_is_set(kyua_fs_concat(&exp_cwd, previous, "root", NULL))); ATF_REQUIRE_STREQ(exp_cwd, cwd); free(exp_cwd); free(cwd); free(previous); }
/// Looks for a core file for the given program. /// /// \param name The basename of the binary that generated the core. /// \param directory The directory from which the program was run. We expect to /// find the core file in this directory. /// \param dead_pid PID of the process that generated the core. This is needed /// in some platforms. /// /// \return The path to the core file if found; otherwise none. char* kyua_stacktrace_find_core(const char* name, const char* directory, const pid_t dead_pid) { char* candidate = NULL; // TODO(jmmv): Other than checking all these defaults, in NetBSD we should // also inspect the value of the kern.defcorename sysctl(2) MIB and use that // as the first candidate. // // In Linux, the way to determine the name is by looking at // /proc/sys/kernel/core_{pattern,uses_pid} as described by core(5). // Unfortunately, there does not seem to be a standard API to parse these // files, which makes checking for core files quite difficult if the // defaults have been modified. // Default NetBSD naming scheme. if (candidate == NULL && MAXCOMLEN > 0) { char truncated[MAXCOMLEN + 1]; candidate = try_core("%s/%s.core", directory, slice(name, truncated, sizeof(truncated))); } // Common naming scheme without the MAXCOMLEN truncation. if (candidate == NULL) candidate = try_core("%s/%s.core", directory, name); // Common naming scheme found in Linux systems. if (candidate == NULL) candidate = try_core("%s/core.%d", directory, (int)dead_pid); // Default Mac OS X naming scheme. if (candidate == NULL) candidate = try_core("/cores/core.%d", (int)dead_pid); // Common naming scheme found in Linux systems. Attempted last due to the // genericity of the core file name. if (candidate == NULL) candidate = try_core("%s/core", directory); if (candidate != NULL) { char* abs_candidate; kyua_error_t error = kyua_fs_make_absolute(candidate, &abs_candidate); if (kyua_error_is_set(error)) { kyua_error_free(error); return candidate; // Return possibly-relative path as a best guess. } else { free(candidate); return abs_candidate; } } else { return candidate; } }
/// Uses kyua_fork, kyua_exec and kyua_wait to execute a subprocess. /// /// \param program Path to the program to run. /// \param args Arguments to the program. /// \param [out] exitstatus The exit status of the subprocess, if it exits /// successfully without timing out nor receiving a signal. /// /// \return Returns the error code of kyua_run_wait (which should have the /// error representation of the exec call in the subprocess). static kyua_error_t exec_check(const char* program, const char* const* args, int* exitstatus) { kyua_run_params_t run_params; kyua_run_params_init(&run_params); pid_t pid; kyua_error_t error = kyua_run_fork(&run_params, &pid); if (!kyua_error_is_set(error) && pid == 0) kyua_run_exec(program, args); ATF_REQUIRE(!kyua_error_is_set(error)); int status; bool timed_out; error = kyua_run_wait(pid, &status, &timed_out); if (!kyua_error_is_set(error)) { ATF_REQUIRE(!timed_out); ATF_REQUIRE_MSG(WIFEXITED(status), "Subprocess expected to exit successfully"); *exitstatus = WEXITSTATUS(status); } return error; }
ATF_TC_BODY(fork_exec_wait__eacces, tc) { ATF_REQUIRE(mkdir("dir", 0000) != -1); const char* const args[] = {"foo", NULL}; int unused_exitstatus; const kyua_error_t error = exec_check("./dir/foo", args, &unused_exitstatus); ATF_REQUIRE(kyua_error_is_set(error)); ATF_REQUIRE(kyua_error_is_type(error, "libc")); ATF_REQUIRE_EQ(EACCES, kyua_libc_error_errno(error)); }
ATF_TC_BODY(fork_wait__timeout, tc) { kyua_run_params_t run_params; kyua_run_params_init(&run_params); run_params.timeout_seconds = 1; pid_t pid; kyua_error_t error = kyua_run_fork(&run_params, &pid); if (!kyua_error_is_set(error) && pid == 0) { sigset_t mask; sigemptyset(&mask); for (;;) sigsuspend(&mask); } ATF_REQUIRE(!kyua_error_is_set(error)); int status; bool timed_out; kyua_run_wait(pid, &status, &timed_out); ATF_REQUIRE(timed_out); ATF_REQUIRE(WIFSIGNALED(status)); ATF_REQUIRE_EQ(SIGKILL, WTERMSIG(status)); }
ATF_TC_BODY(cleanup__subdir__unprotect_regular, tc) { ATF_REQUIRE(mkdir("root", 0755) != -1); ATF_REQUIRE(mkdir("root/dir1", 0755) != -1); ATF_REQUIRE(mkdir("root/dir1/dir2", 0755) != -1); atf_utils_create_file("root/dir1/dir2/file", "%s", ""); ATF_REQUIRE(chmod("root/dir1/dir2/file", 0000) != -1); ATF_REQUIRE(chmod("root/dir1/dir2", 0000) != -1); ATF_REQUIRE(chmod("root/dir1", 0000) != -1); ATF_REQUIRE(!kyua_error_is_set(kyua_fs_cleanup("root"))); ATF_REQUIRE(!lookup(".", "root", DT_DIR)); }