/// Sets an environment variable. /// /// \param name Name of the environment variable to set. /// \param value Value to be set. /// /// \return An error object. kyua_error_t kyua_env_set(const char* name, const char* value) { kyua_error_t error; #if defined(HAVE_SETENV) if (setenv(name, value, 1) == -1) error = kyua_libc_error_new( errno, "Failed to set environment variable %s to %s", name, value); else error = kyua_error_ok(); #elif defined(HAVE_PUTENV) const size_t length = strlen(name) + strlen(value) + 2; char* buffer = (char*)malloc(length); if (buffer == NULL) error = kyua_oom_error_new(); else { const size_t printed_length = snprintf(buffer, length, "%s=%s", name, value); assert(length == printed_length + 1); if (putenv(buffer) == -1) { error = kyua_libc_error_new( errno, "Failed to set environment variable %s to %s", name, value); free(buffer); } else error = kyua_error_ok(); } #else # error "Don't know how to set an environment variable." #endif return error; }
/// Parses the optional argument to a result status. /// /// \param str Pointer to the argument. May be \0 in those cases where the /// status does not have any argument. /// \param [out] status_arg Value of the parsed argument. /// /// \return OK if the argument exists and is valid, or if it does not exist; an /// error otherwise. static kyua_error_t parse_status_arg(const char* str, int* status_arg) { if (*str == '\0') { *status_arg = NO_STATUS_ARG; return kyua_error_ok(); } const size_t length = strlen(str); if (*str != '(' || *(str + length - 1) != ')') return kyua_generic_error_new("Invalid status argument %s", str); const char* const arg = str + 1; char* endptr; const long value = strtol(arg, &endptr, 10); if (arg[0] == '\0' || endptr != str + length - 1) return kyua_generic_error_new("Invalid status argument %s: not a " "number", str); if (errno == ERANGE && (value == LONG_MAX || value == LONG_MIN)) return kyua_generic_error_new("Invalid status argument %s: out of " "range", str); if (value < INT_MIN || value > INT_MAX) return kyua_generic_error_new("Invalid status argument %s: out of " "range", str); *status_arg = (int)value; return kyua_error_ok(); }
/// Extracts the result reason from the input file. /// /// \pre This can only be called for those result types that require a reason. /// /// \param [in,out] input The file from which to read. /// \param first_line The first line of the reason. Because this is part of the /// same line in which the result status is printed, this line has already /// been read by the caller and thus must be provided here. /// \param [out] output Buffer to which to write the full reason. /// \param output_size Size of the output buffer. /// /// \return An error if there was no reason in the input or if there is a /// problem reading it. static kyua_error_t read_reason(FILE* input, const char* first_line, char* output, size_t output_size) { if (first_line == NULL || *first_line == '\0') return kyua_generic_error_new("Test case should have reported a " "failure reason but didn't"); snprintf(output, output_size, "%s", first_line); advance(&output, &output_size); bool had_newline = true; while (!is_really_eof(input)) { if (had_newline) { snprintf(output, output_size, "<<NEWLINE>>"); advance(&output, &output_size); } if (fgets(output, output_size, input) == NULL) { assert(ferror(input)); return kyua_libc_error_new(errno, "Failed to read reason from " "result file"); } had_newline = trim_newline(output); advance(&output, &output_size); } return kyua_error_ok(); }
/// Creates a result file for a failed cleanup routine. /// /// This function is supposed to be invoked after the body has had a chance to /// create its own result file, and only if the body has terminated with a /// non-failure result. /// /// \param output_name Path to the generic result file to create. /// \param wait_status Exit code of the test program as returned by wait(). /// \param timed_out Whether the test program timed out or not. /// \param [out] success Whether the result should be considered a success or /// not; i.e. a clean exit is successful, but anything else is a failure. /// /// \return An error if there is a problem writing the result; OK otherwise. kyua_error_t kyua_atf_result_cleanup_rewrite(const char* output_name, int wait_status, const bool timed_out, bool* success) { if (timed_out) { *success = false; return kyua_result_write( output_name, KYUA_RESULT_BROKEN, "Test case cleanup timed out"); } else { if (WIFEXITED(wait_status)) { if (WEXITSTATUS(wait_status) == EXIT_SUCCESS) { *success = true; // Reuse the result file created by the body. I.e. avoid // creating a new file here. return kyua_error_ok(); } else { *success = false; return kyua_result_write( output_name, KYUA_RESULT_BROKEN, "Test case cleanup exited " "with code %d", WEXITSTATUS(wait_status)); } } else { *success = false; return kyua_result_write( output_name, KYUA_RESULT_BROKEN, "Test case cleanup received " "signal %d%s", WTERMSIG(wait_status), WCOREDUMP(wait_status) ? " (core dumped)" : ""); } } }
/// 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(); }
/// 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; }
/// Generates a string from a format string and its replacements. /// /// \param [out] output Pointer to the dynamically-allocated string with the /// result of the operation. The caller must release this with free(). /// \param format The formatting string. /// \param ap List of to apply to the formatting string. /// /// \return OK if the string could be formatted; an error otherwise. kyua_error_t kyua_text_vprintf(char** output, const char* format, va_list ap) { va_list ap2; va_copy(ap2, ap); const int length = calculate_length(format, ap2); va_end(ap2); if (length < 0) return kyua_libc_error_new(errno, "Could not calculate length of " "string with format '%s'", format); char* buffer = (char*)malloc(length + 1); if (buffer == NULL) return kyua_oom_error_new(); va_copy(ap2, ap); const int printed_length = vsnprintf(buffer, length + 1, format, ap2); va_end(ap2); assert(printed_length == length); if (printed_length < 0) { free(buffer); return kyua_libc_error_new(errno, "Could generate string with format " "'%s'", format); } *output = buffer; return kyua_error_ok(); }
ATF_TC_BODY(error_subsume__secondary, tc) { kyua_error_t primary = kyua_error_ok(); kyua_error_t secondary = kyua_error_new("secondary_error", NULL, 0, NULL); kyua_error_t error = kyua_error_subsume(primary, secondary); ATF_REQUIRE(kyua_error_is_type(error, "secondary_error")); kyua_error_free(error); }
/// 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; }
/// Parses a property from the test cases list. /// /// The property is of the form "name: value", where the value extends to the /// end of the line without quotations. /// /// \param [in,out] line The line to be parsed. This is a non-const pointer /// and the input string is modified to simplify tokenization. /// \param [out] key The name of the property if the parsing succeeds. This /// is a pointer within the input line. /// \param [out] value The value of the property if the parsing succeeds. This /// is a pointer within the input line. /// /// \return OK if the line contains a valid property; an error otherwise. /// In case of success, both key and value are updated. static kyua_error_t parse_property(char* line, char** const key, char** const value) { char* delim = strstr(line, ": "); if (delim == NULL) return kyua_generic_error_new("Invalid property '%s'", line); *delim = '\0'; *(delim + 1) = '\0'; *key = line; *value = delim + 2; return kyua_error_ok(); }
/// Unsets an environment variable. /// /// \param name Name of the environment variable to unset. /// /// \return An error object. kyua_error_t kyua_env_unset(const char* name) { #if defined(HAVE_UNSETENV) if (unsetenv(name) == -1) return kyua_libc_error_new( errno, "Failed to unset environment variable %s", name); else return kyua_error_ok(); #elif defined(HAVE_PUTENV) return kyua_env_set(name, ""); #else # error "Don't know how to unset an environment variable." #endif }
/// Validates that the configuration variables can be set 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. /// /// \return An error if there is a syntax error in the variables. kyua_error_t kyua_env_check_configuration(const char* const user_variables[]) { const char* const* iter; for (iter = user_variables; *iter != NULL; ++iter) { const char* var_value = *iter; if (strlen(var_value) == 0 || (var_value)[0] == '=' || strchr(var_value, '=') == NULL) { return kyua_generic_error_new("Invalid variable '%s'; must be of " "the form var=value", var_value); } } return kyua_error_ok(); }
/// Creates a file with the result of the test. /// /// \param path Path to the file to be created. /// \param type The type of the result. /// /// \return An error object. kyua_error_t kyua_result_write(const char* path, const enum kyua_result_type_t type) { assert(type == KYUA_RESULT_PASSED); FILE* file = fopen(path, "w"); if (file == NULL) return kyua_libc_error_new(errno, "Cannot create result file '%s'", path); fprintf(file, "%s\n", type_to_name[type]); fclose(file); return kyua_error_ok(); }
/// Dumps the contents of the input file into the output. /// /// \param input File from which to read. /// \param output File to which to write. /// /// \return An error if there is a problem. static kyua_error_t dump_file(FILE* input, FILE* output) { char buffer[1024]; size_t length; while ((length = fread(buffer, 1, sizeof(buffer), input)) > 0) { if (fwrite(buffer, 1, length, output) != length) { return kyua_generic_error_new("Failed to write to output file"); } } if (ferror(input)) return kyua_libc_error_new(errno, "Failed to read test cases list"); return kyua_error_ok(); }
/// Reads the header of the test cases list. /// /// The header does not carry any useful information, so all this function does /// is ensure the header is valid. /// /// \param [in,out] input File from which to read the header. /// /// \return OK if the header is valid; an error if it is not. static kyua_error_t parse_header(FILE* input) { char line[80]; // 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, "fgets failed to read test cases list " "header"); if (strcmp(line, TP_LIST_HEADER) != 0) return kyua_generic_error_new("Invalid test cases list header '%s'", line); if (fgets_no_newline(line, sizeof(line), input) == NULL) return fgets_error(input, "fgets failed to read test cases list " "header"); if (strcmp(line, "") != 0) return kyua_generic_error_new("Incomplete test cases list header"); return kyua_error_ok(); }
/// Creates a file with the result of the test. /// /// \param path Path to the file to be created. /// \param type The type of the result. /// \param reason Textual explanation of the reason behind the result. Must be /// NULL with KYUA_RESULT_PASSED, or else non-NULL. /// /// \return An error object. kyua_error_t kyua_result_write_with_reason(const char* path, const enum kyua_result_type_t type, const char* reason, ...) { assert(type != KYUA_RESULT_PASSED); assert(reason != NULL); FILE* file = fopen(path, "w"); if (file == NULL) return kyua_libc_error_new(errno, "Cannot create result file '%s'", path); char buffer[1024]; va_list ap; va_start(ap, reason); (void)vsnprintf(buffer, sizeof(buffer), reason, ap); va_end(ap); fprintf(file, "%s: %s\n", type_to_name[type], buffer); fclose(file); return kyua_error_ok(); }
/// Parses a textual result status. /// /// \param str The text to parse. /// \param [out] status Status type if the input is valid. /// \param [out] status_arg Optional integral argument to the status. /// \param [out] need_reason Whether the detected status requires a reason. /// /// \return An error if the status is not valid. static kyua_error_t parse_status(const char* str, enum atf_status* status, int* status_arg, bool* need_reason) { if (strcmp(str, "passed") == 0) { *status = ATF_STATUS_PASSED; *need_reason = false; return kyua_error_ok(); } else if (strcmp(str, "failed") == 0) { *status = ATF_STATUS_FAILED; *need_reason = true; return kyua_error_ok(); } else if (strcmp(str, "skipped") == 0) { *status = ATF_STATUS_SKIPPED; *need_reason = true; return kyua_error_ok(); } else if (strcmp(str, "expected_death") == 0) { *status = ATF_STATUS_EXPECTED_DEATH; *need_reason = true; return kyua_error_ok(); } else if (strncmp(str, "expected_exit", 13) == 0) { *status = ATF_STATUS_EXPECTED_EXIT; *need_reason = true; return parse_status_arg(str + 13, status_arg); } else if (strcmp(str, "expected_failure") == 0) { *status = ATF_STATUS_EXPECTED_FAILURE; *need_reason = true; return kyua_error_ok(); } else if (strncmp(str, "expected_signal", 15) == 0){ *status = ATF_STATUS_EXPECTED_SIGNAL; *need_reason = true; return parse_status_arg(str + 15, status_arg); } else if (strcmp(str, "expected_timeout") == 0) { *status = ATF_STATUS_EXPECTED_TIMEOUT; *need_reason = true; return kyua_error_ok(); } else { return kyua_generic_error_new("Unknown test case result status %s", str); } }
ATF_TC_BODY(error_subsume__none, tc) { kyua_error_t primary = kyua_error_ok(); kyua_error_t secondary = kyua_error_ok(); ATF_REQUIRE(!kyua_error_is_set(kyua_error_subsume(primary, secondary))); }
ATF_TC_BODY(error_is_set__no, tc) { kyua_error_t error = kyua_error_ok(); ATF_REQUIRE(!kyua_error_is_set(error)); }
/// Gathers a stacktrace of a crashed program. /// /// \param program The name of the binary that crashed and dumped a core file. /// Can be either absolute or relative. /// \param dead_pid The PID of the process that dumped core. /// \param original_run_params Parameters with which the original binary was /// executed. These are reused to run GDB, but adjusted with GDB-specific /// settings. Of special interest, the work directory is used to search for /// the core file. /// \param output Stream into which to dump the stack trace and any additional /// information. /// /// \post If anything goes wrong, the diagnostic messages are written to the /// output. This function returns no errors. void kyua_stacktrace_dump(const char* program, const pid_t dead_pid, const kyua_run_params_t* original_run_params, FILE* output) { fprintf(output, "Process with PID %d dumped core; attempting to gather " "stack trace\n", dead_pid); const kyua_run_params_t run_params = gdb_run_params(original_run_params); kyua_error_t error = kyua_error_ok(); char* core_file = kyua_stacktrace_find_core(const_basename(program), run_params.work_directory, dead_pid); if (core_file == NULL) { fprintf(output, "Cannot find any core file\n"); goto out; } // We must flush the output stream right before invoking fork, so that the // subprocess does not have any unflushed data. Failure to do so results in // the messages above being written twice to the output. fflush(output); pid_t pid; error = kyua_run_fork(&run_params, &pid); if (!kyua_error_is_set(error) && pid == 0) { run_gdb(program, core_file, output); } assert(pid != -1 && pid != 0); if (kyua_error_is_set(error)) goto out_core_file; int status; bool timed_out; error = kyua_run_wait(pid, &status, &timed_out); if (kyua_error_is_set(error)) goto out_core_file; if (timed_out) { fprintf(output, "GDB failed; timed out\n"); } else { if (WIFEXITED(status)) { if (WEXITSTATUS(status) == EXIT_SUCCESS) fprintf(output, "GDB exited successfully\n"); else fprintf(output, "GDB failed with code %d; see output above for " "details\n", WEXITSTATUS(status)); } else { assert(WIFSIGNALED(status)); fprintf(output, "GDB received signal %d; see output above for " "details\n", WTERMSIG(status)); } } out_core_file: free(core_file); out: if (kyua_error_is_set(error)) { kyua_error_fprintf(output, error, "Failed to gather stacktrace"); free(error); } }