/// 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(); }
/// 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; }
/// 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(); }
/// 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; }
/// 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; }
ATF_TC_BODY(generic_error_format__args, tc) { kyua_error_t error = kyua_generic_error_new("%s message %d", "A", 123); char buffer[1024]; kyua_error_format(error, buffer, sizeof(buffer)); ATF_REQUIRE_STREQ("A message 123", buffer); kyua_error_free(error); }
ATF_TC_BODY(generic_error_format__plain, tc) { kyua_error_t error = kyua_generic_error_new("Test message"); char buffer[1024]; kyua_error_format(error, buffer, sizeof(buffer)); ATF_REQUIRE_STREQ("Test message", buffer); kyua_error_free(error); }
/// Generates an error for the case where fgets() returns NULL. /// /// \param input Stream on which fgets() returned an error. /// \param message Error message. /// /// \return An error object with the error message and any relevant details. static kyua_error_t fgets_error(FILE* input, const char* message) { if (feof(input)) { return kyua_generic_error_new("%s: unexpected EOF", message); } else { assert(ferror(input)); return kyua_libc_error_new(errno, "%s", message); } }
/// 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(); }
/// 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(); }
/// 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(); }
/// 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(); }
/// 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(generic_error_type, tc) { kyua_error_t error = kyua_generic_error_new("Nothing"); ATF_REQUIRE(kyua_error_is_type(error, kyua_generic_error_type)); kyua_error_free(error); }
/// Lists the test cases in a test program. /// /// \param test_program Path to the test program for which to list the test /// cases. Should be absolute. /// \param run_params Execution parameters to configure the test process. /// /// \return An error if the listing fails; OK otherwise. static kyua_error_t list_test_cases(const char* test_program, const kyua_run_params_t* run_params) { 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; int stdout_fds[2]; if (pipe(stdout_fds) == -1) { error = kyua_libc_error_new(errno, "pipe failed"); goto out_work_directory; } pid_t pid; error = kyua_run_fork(&real_run_params, &pid); if (!kyua_error_is_set(error) && pid == 0) { run_list(test_program, stdout_fds); } assert(pid != -1 && pid != 0); if (kyua_error_is_set(error)) goto out_stdout_fds; FILE* tmp_output = NULL; // Initialize to shut up gcc warning. error = create_file_in_work_directory(real_run_params.work_directory, "list.txt", "w+", &tmp_output); if (kyua_error_is_set(error)) goto out_stdout_fds; close(stdout_fds[1]); stdout_fds[1] = -1; kyua_error_t parse_error = atf_list_parse(stdout_fds[0], tmp_output); stdout_fds[0] = -1; // Guaranteed closed by atf_list_parse. // Delay reporting of parse errors to later. If we detect a problem while // waiting for the test program, we know that the parsing has most likely // failed and therefore the error with the program is more important for // reporting purposes. int status; bool timed_out; error = kyua_run_wait(pid, &status, &timed_out); if (kyua_error_is_set(error)) goto out_tmp_output; if (!WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS) { error = kyua_generic_error_new("Test program list did not return " "success"); goto out_tmp_output; } error = kyua_error_subsume(error, parse_error); if (!kyua_error_is_set(error)) { rewind(tmp_output); error = dump_file(tmp_output, stdout); } out_tmp_output: fclose(tmp_output); out_stdout_fds: if (stdout_fds[0] != -1) close(stdout_fds[0]); if (stdout_fds[1] != -1) close(stdout_fds[1]); out_work_directory: error = kyua_error_subsume(error, kyua_run_work_directory_leave(&work_directory)); out: return error; }