// Copyright 2012 Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of Google Inc. nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "atf_result.h"
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <errno.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "error.h"
#include "result.h"
// Enumeration of the different result types returned by an ATF test case.
enum atf_status {
ATF_STATUS_EXPECTED_DEATH,
ATF_STATUS_EXPECTED_EXIT,
ATF_STATUS_EXPECTED_FAILURE,
ATF_STATUS_EXPECTED_SIGNAL,
ATF_STATUS_EXPECTED_TIMEOUT,
ATF_STATUS_FAILED,
ATF_STATUS_PASSED,
ATF_STATUS_SKIPPED,
// The broken status below is never returned by the test cases themselves.
// We use it internally to pass around problems detected while dealing with
// the test case itself (like an invalid result file).
ATF_STATUS_BROKEN,
};
/// Magic number representing a missing argument to the test result status.
///
/// Use this to specify that an expected_exit or expected_signal result accepts
/// any exit code or signal, respectively.
#define NO_STATUS_ARG -1
/// Removes a trailing newline from a string (supposedly read by fgets(3)).
///
/// \param [in,out] str The string to remove the trailing newline from.
///
/// \return True if there was a newline character; false otherwise.
static bool
trim_newline(char* str)
{
const size_t length = strlen(str);
if (length == 0) {
return false;
} else {
if (str[length - 1] == '\n') {
str[length - 1] = '\0';
return true;
} else {
return false;
}
}
}
/// Force read on stream to see if we are really at EOF.
///
/// A call to fgets(3) will not return EOF when it returns valid data. But
/// because of our semantics below, we need to be able to tell if more lines are
/// available before actually reading them.
///
/// \param input The stream to check for EOF.
///
/// \return True if the stream is not at EOF yet; false otherwise.
static bool
is_really_eof(FILE* input)
{
const int ch = getc(input);
const bool real_eof = feof(input);
(void)ungetc(ch, input);
return real_eof;
}
/// 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 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);
}
}
/// Advances a pointer to a buffer to its end.
///
/// \param [in,out] buffer Current buffer contents; updated on exit to point to
/// the termination character.
/// \param [in,out] buffer_size Current buffer size; updated on exit to account
/// for the decreased capacity due to the pointer increase.
static void
advance(char** buffer, size_t* buffer_size)
{
const size_t increment = strlen(*buffer);
*buffer += increment;
*buffer_size -= increment;
}
/// 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();
}
/// 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;
}
/// Writes a generic result file for an ATF broken result.
///
/// \param reason Textual explanation of the result.
/// \param status Exit code of the test program as returned by wait().
/// \param output Path to the generic result file to create.
/// \param [out] success Whether the result should be considered a success or
/// not; e.g. passed and skipped are successful, but failed is not.
///
/// \return An error if the conversion fails; OK otherwise.
static kyua_error_t
convert_broken(const char* reason, int status, const char* output,
bool* success)
{
if (WIFEXITED(status)) {
*success = false;
return kyua_result_write(
output, KYUA_RESULT_BROKEN, "%s; test case exited with code %d",
reason, WEXITSTATUS(status));
} else {
assert(WIFSIGNALED(status));
*success = false;
return kyua_result_write(
output, KYUA_RESULT_BROKEN, "%s; test case received signal %d%s",
reason, WTERMSIG(status),
WCOREDUMP(status) ? " (core dumped)" : "");
}
}
/// Writes a generic result file for an ATF expected_death result.
///
/// \param reason Textual explanation of the result.
/// \param status Exit code of the test program as returned by wait().
/// \param output Path to the generic result file to create.
/// \param [out] success Whether the result should be considered a success or
/// not; e.g. passed and skipped are successful, but failed is not.
///
/// \return An error if the conversion fails; OK otherwise.
static kyua_error_t
convert_expected_death(const char* reason, int status, const char* output,
bool* success)
{
if (WIFEXITED(status) && WEXITSTATUS(status) == EXIT_SUCCESS) {
*success = false;
return kyua_result_write(
output, KYUA_RESULT_FAILED, "Test case expected to die but exited "
"successfully");
} else {
*success = true;
return kyua_result_write(
output, KYUA_RESULT_EXPECTED_FAILURE, "%s", reason);
}
}
/// Writes a generic result file for an ATF expected_exit result
///
/// \param status_arg Optional integral argument to the status.
/// \param reason Textual explanation of the result.
/// \param status Exit code of the test program as returned by wait().
/// \param output Path to the generic result file to create.
/// \param [out] success Whether the result should be considered a success or
/// not; e.g. passed and skipped are successful, but failed is not.
///
/// \return An error if the conversion fails; OK otherwise.
static kyua_error_t
convert_expected_exit(const int status_arg, const char* reason, int status,
const char* output, bool* success)
{
if (WIFEXITED(status)) {
if (status_arg == NO_STATUS_ARG || status_arg == WEXITSTATUS(status)) {
*success = true;
return kyua_result_write(
output, KYUA_RESULT_EXPECTED_FAILURE, "%s", reason);
} else {
*success = false;
return kyua_result_write(
output, KYUA_RESULT_FAILED, "Test case expected to exit with "
"code %d but got code %d", status_arg, WEXITSTATUS(status));
}
} else {
assert(WIFSIGNALED(status));
*success = false;
return kyua_result_write(
output, KYUA_RESULT_FAILED, "Test case expected to exit normally "
"but received signal %d%s", WTERMSIG(status),
WCOREDUMP(status) ? " (core dumped)" : "");
}
}
/// Writes a generic result file for an ATF expected_failure result.
///
/// \param reason Textual explanation of the result.
/// \param status Exit code of the test program as returned by wait().
/// \param output Path to the generic result file to create.
/// \param [out] success Whether the result should be considered a success or
/// not; e.g. passed and skipped are successful, but failed is not.
///
/// \return An error if the conversion fails; OK otherwise.
static kyua_error_t
convert_expected_failure(const char* reason, int status, const char* output,
bool* success)
{
if (WIFEXITED(status)) {
if (WEXITSTATUS(status) == EXIT_SUCCESS) {
*success = true;
return kyua_result_write(
output, KYUA_RESULT_EXPECTED_FAILURE, "%s", reason);
} else {
*success = false;
return kyua_result_write(
output, KYUA_RESULT_FAILED, "Test case expected a failure but "
"exited with error code %d", WEXITSTATUS(status));
}
} else {
assert(WIFSIGNALED(status));
*success = false;
return kyua_result_write(
output, KYUA_RESULT_FAILED, "Test case expected a failure but "
"received signal %d%s", WTERMSIG(status),
WCOREDUMP(status) ? " (core dumped)" : "");
}
}
/// Writes a generic result file for an ATF expected_signal result.
///
/// \param status_arg Optional integral argument to the status.
/// \param reason Textual explanation of the result.
/// \param status Exit code of the test program as returned by wait().
/// \param output Path to the generic result file to create.
/// \param [out] success Whether the result should be considered a success or
/// not; e.g. passed and skipped are successful, but failed is not.
///
/// \return An error if the conversion fails; OK otherwise.
static kyua_error_t
convert_expected_signal(const int status_arg, const char* reason, int status,
const char* output, bool* success)
{
if (WIFSIGNALED(status)) {
if (status_arg == NO_STATUS_ARG || status_arg == WTERMSIG(status)) {
*success = true;
return kyua_result_write(
output, KYUA_RESULT_EXPECTED_FAILURE, "%s", reason);
} else {
*success = false;
return kyua_result_write(
output, KYUA_RESULT_FAILED, "Test case expected to receive "
"signal %d but got %d", status_arg, WTERMSIG(status));
}
} else {
assert(WIFEXITED(status));
*success = false;
return kyua_result_write(
output, KYUA_RESULT_FAILED, "Test case expected to receive a "
"signal but exited with code %d", WEXITSTATUS(status));
}
}
/// Writes a generic result file for an ATF expected_timeout result.
///
/// \param status Exit code of the test program as returned by wait().
/// \param output Path to the generic result file to create.
/// \param [out] success Whether the result should be considered a success or
/// not; e.g. passed and skipped are successful, but failed is not.
///
/// \return An error if the conversion fails; OK otherwise.
static kyua_error_t
convert_expected_timeout(int status, const char* output, bool* success)
{
if (WIFEXITED(status)) {
*success = false;
return kyua_result_write(
output, KYUA_RESULT_FAILED, "Test case expected to time out but "
"exited with code %d", WEXITSTATUS(status));
} else {
assert(WIFSIGNALED(status));
*success = false;
return kyua_result_write(
output, KYUA_RESULT_FAILED, "Test case expected to time out but "
"received signal %d%s", WTERMSIG(status),
WCOREDUMP(status) ? " (core dumped)" : "");
}
}
/// Writes a generic result file for an ATF failed result.
///
/// \param reason Textual explanation of the result.
/// \param status Exit code of the test program as returned by wait().
/// \param output Path to the generic result file to create.
/// \param [out] success Whether the result should be considered a success or
/// not; e.g. passed and skipped are successful, but failed is not.
///
/// \return An error if the conversion fails; OK otherwise.
static kyua_error_t
convert_failed(const char* reason, int status, const char* output,
bool* success)
{
if (WIFEXITED(status)) {
if (WEXITSTATUS(status) == EXIT_SUCCESS) {
*success = false;
return kyua_result_write(
output, KYUA_RESULT_BROKEN, "Test case reported a failed "
"result but exited with a successful exit code");
} else {
*success = false;
return kyua_result_write(
output, KYUA_RESULT_FAILED, "%s", reason);
}
} else {
assert(WIFSIGNALED(status));
*success = false;
return kyua_result_write(
output, KYUA_RESULT_BROKEN, "Test case reported a failed result "
"but received signal %d%s", WTERMSIG(status),
WCOREDUMP(status) ? " (core dumped)" : "");
}
}
/// Writes a generic result file for an ATF passed result.
///
/// \param status Exit code of the test program as returned by wait().
/// \param output Path to the generic result file to create.
/// \param [out] success Whether the result should be considered a success or
/// not; e.g. passed and skipped are successful, but failed is not.
///
/// \return An error if the conversion fails; OK otherwise.
static kyua_error_t
convert_passed(int status, const char* output, bool* success)
{
if (WIFEXITED(status)) {
if (WEXITSTATUS(status) == EXIT_SUCCESS) {
*success = true;
return kyua_result_write(output, KYUA_RESULT_PASSED, NULL);
} else {
*success = false;
return kyua_result_write(
output, KYUA_RESULT_BROKEN, "Test case reported a passed "
"result but returned a non-zero exit code %d",
WEXITSTATUS(status));
}
} else {
assert(WIFSIGNALED(status));
*success = false;
return kyua_result_write(
output, KYUA_RESULT_BROKEN, "Test case reported a passed result "
"but received signal %d%s", WTERMSIG(status),
WCOREDUMP(status) ? " (core dumped)" : "");
}
}
/// Writes a generic result file for an ATF skipped result.
///
/// \param reason Textual explanation of the result.
/// \param status Exit code of the test program as returned by wait().
/// \param output Path to the generic result file to create.
/// \param [out] success Whether the result should be considered a success or
/// not; e.g. passed and skipped are successful, but failed is not.
///
/// \return An error if the conversion fails; OK otherwise.
static kyua_error_t
convert_skipped(const char* reason, int status, const char* output,
bool* success)
{
if (WIFEXITED(status)) {
if (WEXITSTATUS(status) == EXIT_SUCCESS) {
*success = true;
return kyua_result_write(output, KYUA_RESULT_SKIPPED, "%s", reason);
} else {
*success = false;
return kyua_result_write(
output, KYUA_RESULT_BROKEN, "Test case reported a skipped "
"result but returned a non-zero exit code %d",
WEXITSTATUS(status));
}
} else {
*success = false;
assert(WIFSIGNALED(status));
return kyua_result_write(
output, KYUA_RESULT_BROKEN, "Test case reported a skipped result "
"but received signal %d%s", WTERMSIG(status),
WCOREDUMP(status) ? " (core dumped)" : "");
}
}
/// Writes a generic result file based on an ATF result and an exit code.
///
/// \param status Type of the ATF result.
/// \param status_arg Optional integral argument to the status.
/// \param reason Textual explanation of the result.
/// \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 output Path to the generic result file to create.
/// \param [out] success Whether the result should be considered a success or
/// not; e.g. passed and skipped are successful, but failed is not.
///
/// \return An error if the conversion fails; OK otherwise.
static kyua_error_t
convert_result(const enum atf_status status, const int status_arg,
const char* reason, const int wait_status, const bool timed_out,
const char* output, bool* success)
{
if (timed_out) {
if (status == ATF_STATUS_EXPECTED_TIMEOUT) {
*success = true;
return kyua_result_write(
output, KYUA_RESULT_EXPECTED_FAILURE, "%s", reason);
} else {
assert(status == ATF_STATUS_BROKEN);
*success = false;
return kyua_result_write(
output, KYUA_RESULT_BROKEN, "Test case body timed out");
}
}
switch (status) {
case ATF_STATUS_BROKEN:
return convert_broken(reason, wait_status, output, success);
case ATF_STATUS_EXPECTED_DEATH:
return convert_expected_death(reason, wait_status, output, success);
case ATF_STATUS_EXPECTED_EXIT:
return convert_expected_exit(status_arg, reason, wait_status, output,
success);
case ATF_STATUS_EXPECTED_FAILURE:
return convert_expected_failure(reason, wait_status, output, success);
case ATF_STATUS_EXPECTED_SIGNAL:
return convert_expected_signal(status_arg, reason, wait_status, output,
success);
case ATF_STATUS_EXPECTED_TIMEOUT:
return convert_expected_timeout(wait_status, output, success);
case ATF_STATUS_FAILED:
return convert_failed(reason, wait_status, output, success);
case ATF_STATUS_PASSED:
return convert_passed(wait_status, output, success);
case ATF_STATUS_SKIPPED:
return convert_skipped(reason, wait_status, output, success);
}
assert(false);
}
/// Writes a generic result file based on an ATF result file and an exit code.
///
/// \param input_name Path to the ATF result file to parse.
/// \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; e.g. passed and skipped are successful, but failed is not.
///
/// \return An error if the conversion fails; OK otherwise.
kyua_error_t
kyua_atf_result_rewrite(const char* input_name, const char* output_name,
const int wait_status, const bool timed_out,
bool* success)
{
enum atf_status status; int status_arg; char reason[1024];
status = ATF_STATUS_BROKEN; // Initialize to shut up gcc warning.
const kyua_error_t error = read_atf_result(input_name, &status, &status_arg,
reason, sizeof(reason));
if (kyua_error_is_set(error)) {
// Errors while parsing the ATF result file can often be attributed to
// the result file being bogus. Therefore, just mark the test case as
// broken, because it possibly is.
status = ATF_STATUS_BROKEN;
kyua_error_format(error, reason, sizeof(reason));
kyua_error_free(error);
}
// Errors converting the loaded result to the final result file are not due
// to a bad test program: they are because our own code fails (e.g. cannot
// create the output file). These need to be returned to the caller.
return convert_result(status, status_arg, reason, wait_status, timed_out,
output_name, success);
}
/// 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)" : "");
}
}
}