/*
 * Copyright (c) 2008-2012 Zmanda, Inc.  All Rights Reserved.
 * Copyright (c) 2013-2016 Carbonite, Inc.  All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 *
 * Contact information: Carbonite Inc., 756 N Pastoria Ave
 * Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
 */

#include "amanda.h"
#include "testutils.h"

gboolean tu_debugging_enabled = FALSE;

static gboolean run_all = TRUE;
static gboolean ignore_timeouts = FALSE;
static gboolean skip_fork = FALSE;
static gboolean only_one = FALSE;
static gboolean loop_forever = FALSE;
static guint64 occurrences = 1;

static void
alarm_hdlr(int sig G_GNUC_UNUSED)
{
    g_fprintf(stderr, "-- TEST TIMED OUT --\n");
    exit(1);
}

/*
 * Run a single test, accouting for the timeout (if timeouts are not ignored)
 * and output runtime information (in milliseconds) at the end of the run.
 * Output avg/min/max only if the number of runs is strictly greater than one.
 */

static gboolean run_one_test(TestUtilsTest *test)
{
    guint64 count = 0;
    gboolean ret = TRUE;
    const char *test_name = test->name;
    GTimer *timer;

    gdouble total = 0.0, thisrun, mintime = G_MAXDOUBLE, maxtime = G_MINDOUBLE;

    signal(SIGALRM, alarm_hdlr);

    timer = g_timer_new();

    while (count++ < occurrences) {
        if (!ignore_timeouts)
            alarm(test->timeout);

        g_timer_start(timer);
        ret = test->fn();
        g_timer_stop(timer);

        thisrun = g_timer_elapsed(timer, NULL);
        total += thisrun;
        if (mintime > thisrun)
            mintime = thisrun;
        if (maxtime < thisrun)
            maxtime = thisrun;

        if (!ret)
            break;
    }

    g_timer_destroy(timer);

    if (loop_forever)
        goto out;

    if (ret) {
        g_fprintf(stderr, " PASS %s (total: %.06f", test_name, total);
        if (occurrences > 1) {
            total /= (gdouble) occurrences;
            g_fprintf(stderr, ", avg/min/max: %.06f/%.06f/%.06f",
                total, mintime, maxtime);
        }
        g_fprintf(stderr, ")\n");
    } else
        g_fprintf(stderr, " FAIL %s (run %ju of %ju, after %.06f secs)\n",
            test_name, (uintmax_t)count, (uintmax_t)occurrences, total);

out:
    return ret;
}

/*
 * Call testfn in a forked process, such that any failures will trigger a
 * test failure, but allow the other tests to proceed. The only exception is if
 * -n is supplied at the command line, but in this case only one test is allowed
 * to run.
 */

static gboolean
callinfork(TestUtilsTest *test)
{
    pid_t pid;
    amwait_t status;
    gboolean result;

    if (skip_fork)
        result = run_one_test(test);
    else {
	switch (pid = fork()) {
	    case 0:	/* child */
		exit(run_one_test(test) ? 0 : 1);

	    case -1:
		perror("fork");
		exit(1);

	    default: /* parent */
		waitpid(pid, &status, 0);
		result = status == 0;
		break;
	}
    }

    return result;
}

static void
usage(
    TestUtilsTest *tests)
{
    printf("USAGE: <test-script> [options] [testname [testname [..]]]\n"
	"\n"
        "Options can be one of:\n"
        "\n"
	"\t-h: this message\n"
	"\t-d: print debugging messages\n"
	"\t-t: ignore timeouts\n"
        "\t-n: do not fork\n"
        "\t-c <count>: run each test <count> times instead of only once\n"
        "\t-l: loop the same test repeatedly (use with -n for leak checks)\n"
	"\n"
	"If no test names are specified, all tests are run.  Available tests:\n"
	"\n");
    while (tests->fn) {
	printf("\t%s\n", tests->name);
	tests++;
    }
}

static void
ignore_debug_messages(
	    const gchar *log_domain G_GNUC_UNUSED,
	    GLogLevelFlags log_level G_GNUC_UNUSED,
	    const gchar *message G_GNUC_UNUSED,
	    gpointer user_data G_GNUC_UNUSED)
{
}

int
testutils_run_tests(
    int argc,
    char **argv,
    TestUtilsTest *tests)
{
    TestUtilsTest *t;
    gboolean success;

    /* first_parse the command line */
    while (argc > 1) {
	if (g_str_equal(argv[1], "-d")) {
	    tu_debugging_enabled = TRUE;
	} else if (g_str_equal(argv[1], "-t")) {
	    ignore_timeouts = TRUE;
	} else if (g_str_equal(argv[1], "-n")) {
	    skip_fork = TRUE;
	    only_one = TRUE;
	} else if (g_str_equal(argv[1], "-l")) {
	    loop_forever = TRUE;
	    only_one = TRUE;
	} else if (g_str_equal(argv[1], "-c")) {
            char *p;
            argv++, argc--;
            occurrences = g_ascii_strtoull(argv[1], &p, 10);
            if (errno == ERANGE) {
                g_fprintf(stderr, "%s is out of range\n", argv[1]);
                exit(1);
            }
            if (*p) {
                g_fprintf(stderr, "The -c option expects a positive integer "
                    "as an argument, but \"%s\" isn't\n", argv[1]);
                exit(1);
            }
            if (occurrences == 0) {
                g_fprintf(stderr, "Sorry, I will not run tests 0 times\n");
                exit(1);
            }
	} else if (g_str_equal(argv[1], "-h")) {
	    usage(tests);
	    return 1;
	} else {
	    int found = 0;

	    for (t = tests; t->fn; t++) {
		if (g_str_equal(argv[1], t->name)) {
		    found = 1;
		    t->selected = 1;
		    break;
		}
	    }

	    if (!found) {
		g_fprintf(stderr, "Test '%s' not found\n", argv[1]);
		return 1;
	    }

	    run_all = FALSE;
	}

	argc--; argv++;
    }

    /*
     * Check whether the -c option has been given. In this case, -l must not be
     * specified at the same time.
     */
    if (occurrences > 1 && loop_forever) {
        g_fprintf(stderr, "-c and -l are incompatible\n");
        exit(1);
    }

    if (run_all) {
        for (t = tests; t->fn; t++)
            t->selected = 1;
    }

    /* check only_one */
    if (only_one) {
        int num_tests = 0;
        for (t = tests; t->fn; t++) {
            if (t->selected)
                num_tests++;
        }

        if (num_tests > 1) {
            g_fprintf(stderr, "Only run one test with '-n'\n");
            return 1;
        }
    }

    /* Make sure g_critical and g_error will exit */
    g_log_set_always_fatal(G_LOG_LEVEL_ERROR |  G_LOG_LEVEL_CRITICAL);

    /* and silently drop debug messages unless we're debugging */
    if (!tu_debugging_enabled) {
	g_log_set_handler(NULL, G_LOG_LEVEL_DEBUG, ignore_debug_messages, NULL);
    }

    /* Now actually run the tests */
    success = TRUE;
    for (t = tests; t->fn; t++) {
        if (t->selected) {
	    do {
		success = callinfork(t) && success;
	    } while (loop_forever);
        }
    }

    return success ? 0 : 1;
}
