/// 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; }
/// 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(); }
/// 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 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(libc_error_format__args, tc) { kyua_error_t error = kyua_libc_error_new(EPERM, "%s message %d", "A", 123); char buffer[1024]; kyua_error_format(error, buffer, sizeof(buffer)); ATF_REQUIRE(strstr(buffer, strerror(EPERM)) != NULL); ATF_REQUIRE(strstr(buffer, "A message 123") != NULL); kyua_error_free(error); }
ATF_TC_BODY(libc_error_format__plain, tc) { kyua_error_t error = kyua_libc_error_new(ENOMEM, "Test message"); char buffer[1024]; kyua_error_format(error, buffer, sizeof(buffer)); ATF_REQUIRE(strstr(buffer, strerror(ENOMEM)) != NULL); ATF_REQUIRE(strstr(buffer, "Test message") != NULL); 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); } }
/// 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 }
/// 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 an ATF test cases list and prints a Kyua definition. /// /// \param fd A file descriptor from which to read the test cases list of a test /// program. Should be connected to the stdout of the latter. This /// function grabs ownership of the descriptor and releases it in all cases. /// \param [in,out] output File to which to write the Kyua definition. /// /// \return OK if the parsing succeeds; an error otherwise. Note that, if there /// is an error, the output may not be consistent and should not be used. kyua_error_t atf_list_parse(const int fd, FILE* output) { kyua_error_t error; FILE* input = fdopen(fd, "r"); if (input == NULL) { error = kyua_libc_error_new(errno, "fdopen(%d) failed", fd); close(fd); } else { error = parse_header(input); if (!kyua_error_is_set(error)) { error = parse_tests(input, output); } fclose(input); } return error; }
/// Creates a file within the work directory. /// /// \param work_directory Path to the work directory. /// \param name Name of the file to create. /// \param mode Mode of the file, as specified by fopen(3). /// \param [out] file Pointer to the created stream. /// /// \return An error if there is a problem. static kyua_error_t create_file_in_work_directory(const char* work_directory, const char* name, const char* mode, FILE** file) { char* path; kyua_error_t error = kyua_fs_concat(&path, work_directory, name, NULL); if (kyua_error_is_set(error)) goto out; FILE* tmp_file = fopen(path, mode); if (tmp_file == NULL) { error = kyua_libc_error_new(errno, "Failed to create %s", path); goto out_path; } *file = tmp_file; assert(!kyua_error_is_set(error)); out_path: free(path); out: return error; }
/// 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(); }
ATF_TC_BODY(libc_error_errno, tc) { kyua_error_t error = kyua_libc_error_new(EPERM, "Doesn't matter"); ATF_REQUIRE_EQ(EPERM, kyua_libc_error_errno(error)); kyua_error_free(error); }
ATF_TC_BODY(libc_error_type, tc) { kyua_error_t error = kyua_libc_error_new(ENOMEM, "Nothing"); ATF_REQUIRE(kyua_error_is_type(error, kyua_libc_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; }