/*
* Automated Testing Framework (atf)
*
* Copyright (c) 2008 The NetBSD Foundation, 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:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. 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.
*
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. 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 FOUNDATION 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 <sys/types.h>
#include <sys/stat.h>
#include <sys/uio.h>

#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "atf-c/defs.h"
#include "atf-c/error.h"
#include "atf-c/tc.h"

#include "detail/env.h"
#include "detail/fs.h"
#include "detail/map.h"
#include "detail/sanity.h"
#include "detail/text.h"

/* ---------------------------------------------------------------------
* Auxiliary functions.
* --------------------------------------------------------------------- */

enum expect_type {
   EXPECT_PASS,
   EXPECT_FAIL,
   EXPECT_EXIT,
   EXPECT_SIGNAL,
   EXPECT_DEATH,
   EXPECT_TIMEOUT,
};

struct context {
   const atf_tc_t *tc;
   const char *resfile;
   size_t fail_count;

   enum expect_type expect;
   atf_dynstr_t expect_reason;
   size_t expect_previous_fail_count;
   size_t expect_fail_count;
   int expect_exitcode;
   int expect_signo;
};

static void context_init(struct context *, const atf_tc_t *, const char *);
static void check_fatal_error(atf_error_t);
static void report_fatal_error(const char *, ...)
   ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(1, 2)
   ATF_DEFS_ATTRIBUTE_NORETURN;
static atf_error_t write_resfile(const int, const char *, const int,
                                const atf_dynstr_t *);
static void create_resfile(const char *, const char *, const int,
                          atf_dynstr_t *);
static void error_in_expect(struct context *, const char *, ...)
   ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(2, 3)
   ATF_DEFS_ATTRIBUTE_NORETURN;
static void validate_expect(struct context *);
static void expected_failure(struct context *, atf_dynstr_t *)
   ATF_DEFS_ATTRIBUTE_NORETURN;
static void fail_requirement(struct context *, atf_dynstr_t *)
   ATF_DEFS_ATTRIBUTE_NORETURN;
static void fail_check(struct context *, atf_dynstr_t *);
static void pass(struct context *)
   ATF_DEFS_ATTRIBUTE_NORETURN;
static void skip(struct context *, atf_dynstr_t *)
   ATF_DEFS_ATTRIBUTE_NORETURN;
static void format_reason_ap(atf_dynstr_t *, const char *, const size_t,
                            const char *, va_list)
   ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(4, 0);
static void format_reason_fmt(atf_dynstr_t *, const char *, const size_t,
                             const char *, ...)
   ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(4, 5);
static void errno_test(struct context *, const char *, const size_t,
                      const int, const char *, const bool,
                      void (*)(struct context *, atf_dynstr_t *));
static atf_error_t check_prog_in_dir(const char *, void *);
static atf_error_t check_prog(struct context *, const char *);

static void
context_init(struct context *ctx, const atf_tc_t *tc, const char *resfile)
{
   ctx->tc = tc;
   ctx->resfile = resfile;
   ctx->fail_count = 0;
   ctx->expect = EXPECT_PASS;
   check_fatal_error(atf_dynstr_init(&ctx->expect_reason));
   ctx->expect_previous_fail_count = 0;
   ctx->expect_fail_count = 0;
   ctx->expect_exitcode = 0;
   ctx->expect_signo = 0;
}

static void
check_fatal_error(atf_error_t err)
{
   if (atf_is_error(err)) {
       char buf[1024];
       atf_error_format(err, buf, sizeof(buf));
       fprintf(stderr, "FATAL ERROR: %s\n", buf);
       atf_error_free(err);
       abort();
   }
}

static void
report_fatal_error(const char *msg, ...)
{
   va_list ap;
   fprintf(stderr, "FATAL ERROR: ");

   va_start(ap, msg);
   vfprintf(stderr, msg, ap);
   va_end(ap);

   fprintf(stderr, "\n");
   abort();
}

/** Writes to a results file.
*
* The results file is supposed to be already open.
*
* This function returns an error code instead of exiting in case of error
* because the caller needs to clean up the reason object before terminating.
*/
static atf_error_t
write_resfile(const int fd, const char *result, const int arg,
             const atf_dynstr_t *reason)
{
   static char NL[] = "\n", CS[] = ": ";
   char buf[64];
   const char *r;
   struct iovec iov[5];
   ssize_t ret;
   int count = 0;

   INV(arg == -1 || reason != NULL);

#define UNCONST(a) ((void *)(unsigned long)(const void *)(a))
   iov[count].iov_base = UNCONST(result);
   iov[count++].iov_len = strlen(result);

   if (reason != NULL) {
       if (arg != -1) {
           iov[count].iov_base = buf;
           iov[count++].iov_len = snprintf(buf, sizeof(buf), "(%d)", arg);
       }

       iov[count].iov_base = CS;
       iov[count++].iov_len = sizeof(CS) - 1;

       r = atf_dynstr_cstring(reason);
       iov[count].iov_base = UNCONST(r);
       iov[count++].iov_len = strlen(r);
   }
#undef UNCONST

   iov[count].iov_base = NL;
   iov[count++].iov_len = sizeof(NL) - 1;

   while ((ret = writev(fd, iov, count)) == -1 && errno == EINTR)
       continue; /* Retry. */
   if (ret != -1)
       return atf_no_error();

   return atf_libc_error(
       errno, "Failed to write results file; result %s, reason %s", result,
       reason == NULL ? "null" : atf_dynstr_cstring(reason));
}

/** Creates a results file.
*
* The input reason is released in all cases.
*
* An error in this function is considered to be fatal, hence why it does
* not return any error code.
*/
static void
create_resfile(const char *resfile, const char *result, const int arg,
              atf_dynstr_t *reason)
{
   atf_error_t err;

   if (strcmp("/dev/stdout", resfile) == 0) {
       err = write_resfile(STDOUT_FILENO, result, arg, reason);
   } else if (strcmp("/dev/stderr", resfile) == 0) {
       err = write_resfile(STDERR_FILENO, result, arg, reason);
   } else {
       const int fd = open(resfile, O_WRONLY | O_CREAT | O_TRUNC,
           S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
       if (fd == -1) {
           err = atf_libc_error(errno, "Cannot create results file '%s'",
                                resfile);
       } else {
           err = write_resfile(fd, result, arg, reason);
           close(fd);
       }
   }

   if (reason != NULL)
       atf_dynstr_fini(reason);

   check_fatal_error(err);
}

/** Fails a test case if validate_expect fails. */
static void
error_in_expect(struct context *ctx, const char *fmt, ...)
{
   atf_dynstr_t reason;
   va_list ap;

   va_start(ap, fmt);
   format_reason_ap(&reason, NULL, 0, fmt, ap);
   va_end(ap);

   ctx->expect = EXPECT_PASS;  /* Ensure fail_requirement really fails. */
   fail_requirement(ctx, &reason);
}

/** Ensures that the "expect" state is correct.
*
* Call this function before modifying the current value of expect.
*/
static void
validate_expect(struct context *ctx)
{
   if (ctx->expect == EXPECT_DEATH) {
       error_in_expect(ctx, "Test case was expected to terminate abruptly "
           "but it continued execution");
   } else if (ctx->expect == EXPECT_EXIT) {
       error_in_expect(ctx, "Test case was expected to exit cleanly but it "
           "continued execution");
   } else if (ctx->expect == EXPECT_FAIL) {
       if (ctx->expect_fail_count == ctx->expect_previous_fail_count)
           error_in_expect(ctx, "Test case was expecting a failure but none "
               "were raised");
       else
           INV(ctx->expect_fail_count > ctx->expect_previous_fail_count);
   } else if (ctx->expect == EXPECT_PASS) {
       /* Nothing to validate. */
   } else if (ctx->expect == EXPECT_SIGNAL) {
       error_in_expect(ctx, "Test case was expected to receive a termination "
           "signal but it continued execution");
   } else if (ctx->expect == EXPECT_TIMEOUT) {
       error_in_expect(ctx, "Test case was expected to hang but it continued "
           "execution");
   } else
       UNREACHABLE;
}

static void
expected_failure(struct context *ctx, atf_dynstr_t *reason)
{
   check_fatal_error(atf_dynstr_prepend_fmt(reason, "%s: ",
       atf_dynstr_cstring(&ctx->expect_reason)));
   create_resfile(ctx->resfile, "expected_failure", -1, reason);
   exit(EXIT_SUCCESS);
}

static void
fail_requirement(struct context *ctx, atf_dynstr_t *reason)
{
   if (ctx->expect == EXPECT_FAIL) {
       expected_failure(ctx, reason);
   } else if (ctx->expect == EXPECT_PASS) {
       create_resfile(ctx->resfile, "failed", -1, reason);
       exit(EXIT_FAILURE);
   } else {
       error_in_expect(ctx, "Test case raised a failure but was not "
           "expecting one; reason was %s", atf_dynstr_cstring(reason));
   }
   UNREACHABLE;
}

static void
fail_check(struct context *ctx, atf_dynstr_t *reason)
{
   if (ctx->expect == EXPECT_FAIL) {
       fprintf(stderr, "*** Expected check failure: %s: %s\n",
           atf_dynstr_cstring(&ctx->expect_reason),
           atf_dynstr_cstring(reason));
       ctx->expect_fail_count++;
   } else if (ctx->expect == EXPECT_PASS) {
       fprintf(stderr, "*** Check failed: %s\n", atf_dynstr_cstring(reason));
       ctx->fail_count++;
   } else {
       error_in_expect(ctx, "Test case raised a failure but was not "
           "expecting one; reason was %s", atf_dynstr_cstring(reason));
   }

   atf_dynstr_fini(reason);
}

static void
pass(struct context *ctx)
{
   if (ctx->expect == EXPECT_FAIL) {
       error_in_expect(ctx, "Test case was expecting a failure but got "
           "a pass instead");
   } else if (ctx->expect == EXPECT_PASS) {
       create_resfile(ctx->resfile, "passed", -1, NULL);
       exit(EXIT_SUCCESS);
   } else {
       error_in_expect(ctx, "Test case asked to explicitly pass but was "
           "not expecting such condition");
   }
   UNREACHABLE;
}

static void
skip(struct context *ctx, atf_dynstr_t *reason)
{
   if (ctx->expect == EXPECT_PASS) {
       create_resfile(ctx->resfile, "skipped", -1, reason);
       exit(EXIT_SUCCESS);
   } else {
       error_in_expect(ctx, "Can only skip a test case when running in "
           "expect pass mode");
   }
   UNREACHABLE;
}

/** Formats a failure/skip reason message.
*
* The formatted reason is stored in out_reason.  out_reason is initialized
* in this function and is supposed to be released by the caller.  In general,
* the reason will eventually be fed to create_resfile, which will release
* it.
*
* Errors in this function are fatal.  Rationale being: reasons are used to
* create results files; if we can't format the reason correctly, the result
* of the test program will be bogus.  So it's better to just exit with a
* fatal error.
*/
static void
format_reason_ap(atf_dynstr_t *out_reason,
                const char *source_file, const size_t source_line,
                const char *reason, va_list ap)
{
   atf_error_t err;

   if (source_file != NULL) {
       err = atf_dynstr_init_fmt(out_reason, "%s:%zd: ", source_file,
                                 source_line);
   } else {
       PRE(source_line == 0);
       err = atf_dynstr_init(out_reason);
   }

   if (!atf_is_error(err)) {
       va_list ap2;
       va_copy(ap2, ap);
       err = atf_dynstr_append_ap(out_reason, reason, ap2);
       va_end(ap2);
   }

   check_fatal_error(err);
}

static void
format_reason_fmt(atf_dynstr_t *out_reason,
                 const char *source_file, const size_t source_line,
                 const char *reason, ...)
{
   va_list ap;

   va_start(ap, reason);
   format_reason_ap(out_reason, source_file, source_line, reason, ap);
   va_end(ap);
}

static void
errno_test(struct context *ctx, const char *file, const size_t line,
          const int exp_errno, const char *expr_str,
          const bool expr_result,
          void (*fail_func)(struct context *, atf_dynstr_t *))
{
   const int actual_errno = errno;

   if (expr_result) {
       if (exp_errno != actual_errno) {
           atf_dynstr_t reason;

           format_reason_fmt(&reason, file, line, "Expected errno %d, got %d, "
               "in %s", exp_errno, actual_errno, expr_str);
           fail_func(ctx, &reason);
       }
   } else {
       atf_dynstr_t reason;

       format_reason_fmt(&reason, file, line, "Expected true value in %s",
           expr_str);
       fail_func(ctx, &reason);
   }
}

struct prog_found_pair {
   const char *prog;
   bool found;
};

static atf_error_t
check_prog_in_dir(const char *dir, void *data)
{
   struct prog_found_pair *pf = data;
   atf_error_t err;

   if (pf->found)
       err = atf_no_error();
   else {
       atf_fs_path_t p;

       err = atf_fs_path_init_fmt(&p, "%s/%s", dir, pf->prog);
       if (atf_is_error(err))
           goto out_p;

       err = atf_fs_eaccess(&p, atf_fs_access_x);
       if (!atf_is_error(err))
           pf->found = true;
       else {
           atf_error_free(err);
           INV(!pf->found);
           err = atf_no_error();
       }

out_p:
       atf_fs_path_fini(&p);
   }

   return err;
}

static atf_error_t
check_prog(struct context *ctx, const char *prog)
{
   atf_error_t err;
   atf_fs_path_t p;

   err = atf_fs_path_init_fmt(&p, "%s", prog);
   if (atf_is_error(err))
       goto out;

   if (atf_fs_path_is_absolute(&p)) {
       err = atf_fs_eaccess(&p, atf_fs_access_x);
       if (atf_is_error(err)) {
           atf_dynstr_t reason;

           atf_error_free(err);
           atf_fs_path_fini(&p);
           format_reason_fmt(&reason, NULL, 0, "The required program %s could "
               "not be found", prog);
           skip(ctx, &reason);
       }
   } else {
       const char *path = atf_env_get("PATH");
       struct prog_found_pair pf;
       atf_fs_path_t bp;

       err = atf_fs_path_branch_path(&p, &bp);
       if (atf_is_error(err))
           goto out_p;

       if (strcmp(atf_fs_path_cstring(&bp), ".") != 0) {
           atf_fs_path_fini(&bp);
           atf_fs_path_fini(&p);

           report_fatal_error("Relative paths are not allowed when searching "
               "for a program (%s)", prog);
           UNREACHABLE;
       }

       pf.prog = prog;
       pf.found = false;
       err = atf_text_for_each_word(path, ":", check_prog_in_dir, &pf);
       if (atf_is_error(err))
           goto out_bp;

       if (!pf.found) {
           atf_dynstr_t reason;

           atf_fs_path_fini(&bp);
           atf_fs_path_fini(&p);
           format_reason_fmt(&reason, NULL, 0, "The required program %s could "
               "not be found in the PATH", prog);
           fail_requirement(ctx, &reason);
       }

out_bp:
       atf_fs_path_fini(&bp);
   }

out_p:
   atf_fs_path_fini(&p);
out:
   return err;
}

/* ---------------------------------------------------------------------
* The "atf_tc" type.
* --------------------------------------------------------------------- */

struct atf_tc_impl {
   const char *m_ident;

   atf_map_t m_vars;
   atf_map_t m_config;

   atf_tc_head_t m_head;
   atf_tc_body_t m_body;
   atf_tc_cleanup_t m_cleanup;
};

/*
* Constructors/destructors.
*/

atf_error_t
atf_tc_init(atf_tc_t *tc, const char *ident, atf_tc_head_t head,
           atf_tc_body_t body, atf_tc_cleanup_t cleanup,
           const char *const *config)
{
   atf_error_t err;

   tc->pimpl = malloc(sizeof(struct atf_tc_impl));
   if (tc->pimpl == NULL) {
       err = atf_no_memory_error();
       goto err;
   }

   tc->pimpl->m_ident = ident;
   tc->pimpl->m_head = head;
   tc->pimpl->m_body = body;
   tc->pimpl->m_cleanup = cleanup;

   err = atf_map_init_charpp(&tc->pimpl->m_config, config);
   if (atf_is_error(err))
       goto err;

   err = atf_map_init(&tc->pimpl->m_vars);
   if (atf_is_error(err))
       goto err_vars;

   err = atf_tc_set_md_var(tc, "ident", "%s", ident);
   if (atf_is_error(err))
       goto err_map;

   if (cleanup != NULL) {
       err = atf_tc_set_md_var(tc, "has.cleanup", "true");
       if (atf_is_error(err))
           goto err_map;
   }

   /* XXX Should the head be able to return error codes? */
   if (tc->pimpl->m_head != NULL)
       tc->pimpl->m_head(tc);

   if (strcmp(atf_tc_get_md_var(tc, "ident"), ident) != 0) {
       report_fatal_error("Test case head modified the read-only 'ident' "
           "property");
       UNREACHABLE;
   }

   INV(!atf_is_error(err));
   return err;

err_map:
   atf_map_fini(&tc->pimpl->m_vars);
err_vars:
   atf_map_fini(&tc->pimpl->m_config);
err:
   return err;
}

atf_error_t
atf_tc_init_pack(atf_tc_t *tc, const atf_tc_pack_t *pack,
                const char *const *config)
{
   return atf_tc_init(tc, pack->m_ident, pack->m_head, pack->m_body,
                      pack->m_cleanup, config);
}

void
atf_tc_fini(atf_tc_t *tc)
{
   atf_map_fini(&tc->pimpl->m_vars);
   free(tc->pimpl);
}

/*
* Getters.
*/

const char *
atf_tc_get_ident(const atf_tc_t *tc)
{
   return tc->pimpl->m_ident;
}

const char *
atf_tc_get_config_var(const atf_tc_t *tc, const char *name)
{
   const char *val;
   atf_map_citer_t iter;

   PRE(atf_tc_has_config_var(tc, name));
   iter = atf_map_find_c(&tc->pimpl->m_config, name);
   val = atf_map_citer_data(iter);
   INV(val != NULL);

   return val;
}

const char *
atf_tc_get_config_var_wd(const atf_tc_t *tc, const char *name,
                        const char *defval)
{
   const char *val;

   if (!atf_tc_has_config_var(tc, name))
       val = defval;
   else
       val = atf_tc_get_config_var(tc, name);

   return val;
}

bool
atf_tc_get_config_var_as_bool(const atf_tc_t *tc, const char *name)
{
   bool val;
   const char *strval;
   atf_error_t err;

   strval = atf_tc_get_config_var(tc, name);
   err = atf_text_to_bool(strval, &val);
   if (atf_is_error(err)) {
       atf_error_free(err);
       atf_tc_fail("Configuration variable %s does not have a valid "
                   "boolean value; found %s", name, strval);
   }

   return val;
}

bool
atf_tc_get_config_var_as_bool_wd(const atf_tc_t *tc, const char *name,
                                const bool defval)
{
   bool val;

   if (!atf_tc_has_config_var(tc, name))
       val = defval;
   else
       val = atf_tc_get_config_var_as_bool(tc, name);

   return val;
}

long
atf_tc_get_config_var_as_long(const atf_tc_t *tc, const char *name)
{
   long val;
   const char *strval;
   atf_error_t err;

   strval = atf_tc_get_config_var(tc, name);
   err = atf_text_to_long(strval, &val);
   if (atf_is_error(err)) {
       atf_error_free(err);
       atf_tc_fail("Configuration variable %s does not have a valid "
                   "long value; found %s", name, strval);
   }

   return val;
}

long
atf_tc_get_config_var_as_long_wd(const atf_tc_t *tc, const char *name,
                                const long defval)
{
   long val;

   if (!atf_tc_has_config_var(tc, name))
       val = defval;
   else
       val = atf_tc_get_config_var_as_long(tc, name);

   return val;
}

const char *
atf_tc_get_md_var(const atf_tc_t *tc, const char *name)
{
   const char *val;
   atf_map_citer_t iter;

   PRE(atf_tc_has_md_var(tc, name));
   iter = atf_map_find_c(&tc->pimpl->m_vars, name);
   val = atf_map_citer_data(iter);
   INV(val != NULL);

   return val;
}

char **
atf_tc_get_md_vars(const atf_tc_t *tc)
{
   return atf_map_to_charpp(&tc->pimpl->m_vars);
}

bool
atf_tc_has_config_var(const atf_tc_t *tc, const char *name)
{
   atf_map_citer_t end, iter;

   iter = atf_map_find_c(&tc->pimpl->m_config, name);
   end = atf_map_end_c(&tc->pimpl->m_config);
   return !atf_equal_map_citer_map_citer(iter, end);
}

bool
atf_tc_has_md_var(const atf_tc_t *tc, const char *name)
{
   atf_map_citer_t end, iter;

   iter = atf_map_find_c(&tc->pimpl->m_vars, name);
   end = atf_map_end_c(&tc->pimpl->m_vars);
   return !atf_equal_map_citer_map_citer(iter, end);
}

/*
* Modifiers.
*/

atf_error_t
atf_tc_set_md_var(atf_tc_t *tc, const char *name, const char *fmt, ...)
{
   atf_error_t err;
   char *value;
   va_list ap;

   va_start(ap, fmt);
   err = atf_text_format_ap(&value, fmt, ap);
   va_end(ap);

   if (!atf_is_error(err))
       err = atf_map_insert(&tc->pimpl->m_vars, name, value, true);
   else
       free(value);

   return err;
}

/* ---------------------------------------------------------------------
* Free functions, as they should be publicly but they can't.
* --------------------------------------------------------------------- */

static void _atf_tc_fail(struct context *, const char *, va_list)
   ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(2, 0)
   ATF_DEFS_ATTRIBUTE_NORETURN;
static void _atf_tc_fail_nonfatal(struct context *, const char *, va_list)
   ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(2, 0);
static void _atf_tc_fail_check(struct context *, const char *, const size_t,
   const char *, va_list)
   ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(4, 0);
static void _atf_tc_fail_requirement(struct context *, const char *,
   const size_t, const char *, va_list)
   ATF_DEFS_ATTRIBUTE_NORETURN
   ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(4, 0);
static void _atf_tc_pass(struct context *) ATF_DEFS_ATTRIBUTE_NORETURN;
static void _atf_tc_require_prog(struct context *, const char *);
static void _atf_tc_skip(struct context *, const char *, va_list)
   ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(2, 0)
   ATF_DEFS_ATTRIBUTE_NORETURN;
static void _atf_tc_check_errno(struct context *, const char *, const size_t,
   const int, const char *, const bool);
static void _atf_tc_require_errno(struct context *, const char *, const size_t,
   const int, const char *, const bool);
static void _atf_tc_expect_pass(struct context *);
static void _atf_tc_expect_fail(struct context *, const char *, va_list)
   ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(2, 0);
static void _atf_tc_expect_exit(struct context *, const int, const char *,
   va_list) ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(3, 0);
static void _atf_tc_expect_signal(struct context *, const int, const char *,
   va_list) ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(3, 0);
static void _atf_tc_expect_death(struct context *, const char *,
   va_list) ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(2, 0);

static void
_atf_tc_fail(struct context *ctx, const char *fmt, va_list ap)
{
   va_list ap2;
   atf_dynstr_t reason;

   va_copy(ap2, ap);
   format_reason_ap(&reason, NULL, 0, fmt, ap2);
   va_end(ap2);

   fail_requirement(ctx, &reason);
   UNREACHABLE;
}

static void
_atf_tc_fail_nonfatal(struct context *ctx, const char *fmt, va_list ap)
{
   va_list ap2;
   atf_dynstr_t reason;

   va_copy(ap2, ap);
   format_reason_ap(&reason, NULL, 0, fmt, ap2);
   va_end(ap2);

   fail_check(ctx, &reason);
}

static void
_atf_tc_fail_check(struct context *ctx, const char *file, const size_t line,
                  const char *fmt, va_list ap)
{
   va_list ap2;
   atf_dynstr_t reason;

   va_copy(ap2, ap);
   format_reason_ap(&reason, file, line, fmt, ap2);
   va_end(ap2);

   fail_check(ctx, &reason);
}

static void
_atf_tc_fail_requirement(struct context *ctx, const char *file,
                        const size_t line, const char *fmt, va_list ap)
{
   va_list ap2;
   atf_dynstr_t reason;

   va_copy(ap2, ap);
   format_reason_ap(&reason, file, line, fmt, ap2);
   va_end(ap2);

   fail_requirement(ctx, &reason);
   UNREACHABLE;
}

static void
_atf_tc_pass(struct context *ctx)
{
   pass(ctx);
   UNREACHABLE;
}

static void
_atf_tc_require_prog(struct context *ctx, const char *prog)
{
   check_fatal_error(check_prog(ctx, prog));
}

static void
_atf_tc_skip(struct context *ctx, const char *fmt, va_list ap)
{
   atf_dynstr_t reason;
   va_list ap2;

   va_copy(ap2, ap);
   format_reason_ap(&reason, NULL, 0, fmt, ap2);
   va_end(ap2);

   skip(ctx, &reason);
}

static void
_atf_tc_check_errno(struct context *ctx, const char *file, const size_t line,
                   const int exp_errno, const char *expr_str,
                   const bool expr_result)
{
   errno_test(ctx, file, line, exp_errno, expr_str, expr_result, fail_check);
}

static void
_atf_tc_require_errno(struct context *ctx, const char *file, const size_t line,
                     const int exp_errno, const char *expr_str,
                     const bool expr_result)
{
   errno_test(ctx, file, line, exp_errno, expr_str, expr_result,
       fail_requirement);
}

static void
_atf_tc_expect_pass(struct context *ctx)
{
   validate_expect(ctx);

   ctx->expect = EXPECT_PASS;
}

static void
_atf_tc_expect_fail(struct context *ctx, const char *reason, va_list ap)
{
   va_list ap2;

   validate_expect(ctx);

   ctx->expect = EXPECT_FAIL;
   atf_dynstr_fini(&ctx->expect_reason);
   va_copy(ap2, ap);
   check_fatal_error(atf_dynstr_init_ap(&ctx->expect_reason, reason, ap2));
   va_end(ap2);
   ctx->expect_previous_fail_count = ctx->expect_fail_count;
}

static void
_atf_tc_expect_exit(struct context *ctx, const int exitcode, const char *reason,
                   va_list ap)
{
   va_list ap2;
   atf_dynstr_t formatted;

   validate_expect(ctx);

   ctx->expect = EXPECT_EXIT;
   va_copy(ap2, ap);
   check_fatal_error(atf_dynstr_init_ap(&formatted, reason, ap2));
   va_end(ap2);

   create_resfile(ctx->resfile, "expected_exit", exitcode, &formatted);
}

static void
_atf_tc_expect_signal(struct context *ctx, const int signo, const char *reason,
                     va_list ap)
{
   va_list ap2;
   atf_dynstr_t formatted;

   validate_expect(ctx);

   ctx->expect = EXPECT_SIGNAL;
   va_copy(ap2, ap);
   check_fatal_error(atf_dynstr_init_ap(&formatted, reason, ap2));
   va_end(ap2);

   create_resfile(ctx->resfile, "expected_signal", signo, &formatted);
}

static void
_atf_tc_expect_death(struct context *ctx, const char *reason, va_list ap)
{
   va_list ap2;
   atf_dynstr_t formatted;

   validate_expect(ctx);

   ctx->expect = EXPECT_DEATH;
   va_copy(ap2, ap);
   check_fatal_error(atf_dynstr_init_ap(&formatted, reason, ap2));
   va_end(ap2);

   create_resfile(ctx->resfile, "expected_death", -1, &formatted);
}

ATF_DEFS_ATTRIBUTE_FORMAT_PRINTF(2, 0)
static void
_atf_tc_expect_timeout(struct context *ctx, const char *reason, va_list ap)
{
   va_list ap2;
   atf_dynstr_t formatted;

   validate_expect(ctx);

   ctx->expect = EXPECT_TIMEOUT;
   va_copy(ap2, ap);
   check_fatal_error(atf_dynstr_init_ap(&formatted, reason, ap2));
   va_end(ap2);

   create_resfile(ctx->resfile, "expected_timeout", -1, &formatted);
}

/* ---------------------------------------------------------------------
* Free functions.
* --------------------------------------------------------------------- */

static struct context Current;

atf_error_t
atf_tc_run(const atf_tc_t *tc, const char *resfile)
{
   context_init(&Current, tc, resfile);

   tc->pimpl->m_body(tc);

   validate_expect(&Current);

   if (Current.fail_count > 0) {
       atf_dynstr_t reason;

       format_reason_fmt(&reason, NULL, 0, "%zu checks failed; see output for "
           "more details", Current.fail_count);
       fail_requirement(&Current, &reason);
   } else if (Current.expect_fail_count > 0) {
       atf_dynstr_t reason;

       format_reason_fmt(&reason, NULL, 0, "%zu checks failed as expected; "
           "see output for more details", Current.expect_fail_count);
       expected_failure(&Current, &reason);
   } else {
       pass(&Current);
   }
   UNREACHABLE;
   return atf_no_error();
}

atf_error_t
atf_tc_cleanup(const atf_tc_t *tc)
{
   if (tc->pimpl->m_cleanup != NULL)
       tc->pimpl->m_cleanup(tc);
   return atf_no_error(); /* XXX */
}

/* ---------------------------------------------------------------------
* Free functions that depend on Current.
* --------------------------------------------------------------------- */

/*
* All the functions below provide delegates to other internal functions
* (prefixed by _) that take the current test case as an argument to
* prevent them from accessing global state.  This is to keep the side-
* effects of the internal functions clearer and easier to understand.
*
* The public API should never have hid the fact that it needs access to
* the current test case (other than maybe in the macros), but changing it
* is hard.  TODO: Revisit in the future.
*/

void
atf_tc_fail(const char *fmt, ...)
{
   va_list ap;

   PRE(Current.tc != NULL);

   va_start(ap, fmt);
   _atf_tc_fail(&Current, fmt, ap);
   va_end(ap);
}

void
atf_tc_fail_nonfatal(const char *fmt, ...)
{
   va_list ap;

   PRE(Current.tc != NULL);

   va_start(ap, fmt);
   _atf_tc_fail_nonfatal(&Current, fmt, ap);
   va_end(ap);
}

void
atf_tc_fail_check(const char *file, const size_t line, const char *fmt, ...)
{
   va_list ap;

   PRE(Current.tc != NULL);

   va_start(ap, fmt);
   _atf_tc_fail_check(&Current, file, line, fmt, ap);
   va_end(ap);
}

void
atf_tc_fail_requirement(const char *file, const size_t line,
                       const char *fmt, ...)
{
   va_list ap;

   PRE(Current.tc != NULL);

   va_start(ap, fmt);
   _atf_tc_fail_requirement(&Current, file, line, fmt, ap);
   va_end(ap);
}

void
atf_tc_pass(void)
{
   PRE(Current.tc != NULL);

   _atf_tc_pass(&Current);
}

void
atf_tc_require_prog(const char *prog)
{
   PRE(Current.tc != NULL);

   _atf_tc_require_prog(&Current, prog);
}

void
atf_tc_skip(const char *fmt, ...)
{
   va_list ap;

   PRE(Current.tc != NULL);

   va_start(ap, fmt);
   _atf_tc_skip(&Current, fmt, ap);
   va_end(ap);
}

void
atf_tc_check_errno(const char *file, const size_t line, const int exp_errno,
                  const char *expr_str, const bool expr_result)
{
   PRE(Current.tc != NULL);

   _atf_tc_check_errno(&Current, file, line, exp_errno, expr_str,
                       expr_result);
}

void
atf_tc_require_errno(const char *file, const size_t line, const int exp_errno,
                    const char *expr_str, const bool expr_result)
{
   PRE(Current.tc != NULL);

   _atf_tc_require_errno(&Current, file, line, exp_errno, expr_str,
                         expr_result);
}

void
atf_tc_expect_pass(void)
{
   PRE(Current.tc != NULL);

   _atf_tc_expect_pass(&Current);
}

void
atf_tc_expect_fail(const char *reason, ...)
{
   va_list ap;

   PRE(Current.tc != NULL);

   va_start(ap, reason);
   _atf_tc_expect_fail(&Current, reason, ap);
   va_end(ap);
}

void
atf_tc_expect_exit(const int exitcode, const char *reason, ...)
{
   va_list ap;

   PRE(Current.tc != NULL);

   va_start(ap, reason);
   _atf_tc_expect_exit(&Current, exitcode, reason, ap);
   va_end(ap);
}

void
atf_tc_expect_signal(const int signo, const char *reason, ...)
{
   va_list ap;

   PRE(Current.tc != NULL);

   va_start(ap, reason);
   _atf_tc_expect_signal(&Current, signo, reason, ap);
   va_end(ap);
}

void
atf_tc_expect_death(const char *reason, ...)
{
   va_list ap;

   PRE(Current.tc != NULL);

   va_start(ap, reason);
   _atf_tc_expect_death(&Current, reason, ap);
   va_end(ap);
}

void
atf_tc_expect_timeout(const char *reason, ...)
{
   va_list ap;

   PRE(Current.tc != NULL);

   va_start(ap, reason);
   _atf_tc_expect_timeout(&Current, reason, ap);
   va_end(ap);
}