diff options
Diffstat (limited to 'test/libtest/driver/driver_main.c')
-rw-r--r-- | test/libtest/driver/driver_main.c | 726 |
1 files changed, 726 insertions, 0 deletions
diff --git a/test/libtest/driver/driver_main.c b/test/libtest/driver/driver_main.c new file mode 100644 index 000000000000..c43ccf52ca15 --- /dev/null +++ b/test/libtest/driver/driver_main.c @@ -0,0 +1,726 @@ +/*- + * Copyright (c) 2018, Joseph Koshy + * 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 + * in this position and unchanged. + * 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 AUTHOR(S) ``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 AUTHOR(S) 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. + */ + +/* + * This file defines a "main()" that invokes (or lists) the tests that were + * linked into the current executable. + */ + +#include <sys/param.h> +#include <sys/queue.h> +#include <sys/stat.h> + +#include <assert.h> +#include <err.h> +#include <errno.h> +#include <fnmatch.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sysexits.h> +#include <time.h> +#include <unistd.h> + +#include "_elftc.h" + +#include "test.h" +#include "test_case.h" + +#include "driver.h" + +#if defined(ELFTC_VCSID) +ELFTC_VCSID("$Id$"); +#endif + +enum selection_scope { + SCOPE_TEST_CASE = 0, /* c:STRING */ + SCOPE_TEST_FUNCTION, /* f:STRING */ + SCOPE_TAG, /* t:STRING */ +}; + +/* Selection list entry. */ +struct selection_option { + STAILQ_ENTRY(selection_option) so_next; + + /* The text to use for matching. */ + const char *so_pattern; + + /* + * Whether matched test and test cases should be selected + * (if false) or deselected (if true). + */ + bool so_select_tests; + + /* The kind of information to match. */ + enum selection_scope so_selection_scope; +}; + +/* All selection options specified. */ +STAILQ_HEAD(selection_option_list, selection_option); + +static struct selection_option * +parse_selection_option(const char *option) +{ + int scope_char; + bool select_tests; + enum selection_scope scope; + struct selection_option *so; + + scope_char = '\0'; + select_tests = true; + scope = SCOPE_TEST_CASE; + + /* Deselection patterns start with a '-'. */ + if (*option == '-') { + select_tests = false; + option++; + } + + /* + * If a scope was not specified, the selection scope defaults + * to SCOPE_TEST_CASE. + */ + if (strchr(option, ':') == NULL) + scope_char = 'c'; + else { + scope_char = *option++; + if (*option != ':') + return (NULL); + option++; /* Skip over the ':'. */ + } + + if (*option == '\0') + return (NULL); + + switch (scope_char) { + case 'c': + scope = SCOPE_TEST_CASE; + break; + case 'f': + scope = SCOPE_TEST_FUNCTION; + break; + case 't': + scope = SCOPE_TAG; + break; + default: + return (NULL); + } + + so = calloc(1, sizeof(*so)); + so->so_pattern = option; + so->so_selection_scope = scope; + so->so_select_tests = select_tests; + + return (so); +} + +/* Test execution styles. */ +struct style_entry { + enum test_run_style se_style; + const char *se_name; +}; + +static const struct style_entry known_styles[] = { + { TR_STYLE_LIBTEST, "libtest" }, + { TR_STYLE_TAP, "tap" }, + { TR_STYLE_ATF, "atf" } +}; + +/* + * Parse a test run style. + * + * This function returns true if the run style was recognized, or + * false otherwise. + */ +static bool +parse_run_style(const char *option, enum test_run_style *run_style) +{ + size_t n; + + for (n = 0; n < sizeof(known_styles) / sizeof(known_styles[0]); n++) { + if (strcasecmp(option, known_styles[n].se_name) == 0) { + *run_style = known_styles[n].se_style; + return (true); + } + } + + return (false); +} + +/* + * Return the canonical spelling of a test execution style. + */ +static const char * +to_execution_style_name(enum test_run_style run_style) +{ + size_t n; + + for (n = 0; n < sizeof(known_styles) / sizeof(known_styles[0]); n++) { + if (known_styles[n].se_style == run_style) + return (known_styles[n].se_name); + } + + return (NULL); +} + +/* + * Parse a string value containing a positive integral number. + */ +static bool +parse_execution_time(const char *option, long *execution_time) { + char *end; + long value; + + if (option == NULL || *option == '\0') + return (false); + + value = strtol(option, &end, 10); + + /* Check for parse errors. */ + if (*end != '\0') + return (false); + + /* Reject negative numbers. */ + if (value < 0) + return (false); + + /* Check for overflows during parsing. */ + if (value == LONG_MAX && errno == ERANGE) + return (false); + + *execution_time = value; + + return (true); +} + +/* + * Match the names of test cases. + * + * In the event of a match, then the selection state specifed in + * 'option' is applied to all the test functions in the test case. + */ +static void +match_test_cases(struct selection_option *option, + struct test_case_selector *tcs) +{ + const struct test_case_descriptor *tcd; + struct test_function_selector *tfs; + + tcd = tcs->tcs_descriptor; + + if (fnmatch(option->so_pattern, tcd->tc_name, 0)) + return; + + STAILQ_FOREACH(tfs, &tcs->tcs_functions, tfs_next) + tfs->tfs_is_selected = option->so_select_tests; +} + +/* + * Match the names of test functions. + */ +static void +match_test_functions(struct selection_option *option, + struct test_case_selector *tcs) +{ + struct test_function_selector *tfs; + const struct test_function_descriptor *tfd; + + STAILQ_FOREACH(tfs, &tcs->tcs_functions, tfs_next) { + tfd = tfs->tfs_descriptor; + + if (fnmatch(option->so_pattern, tfd->tf_name, 0)) + continue; + + tfs->tfs_is_selected = option->so_select_tests; + } +} + +/* + * Helper: returns true if the specified text matches any of the + * entries in the array 'tags'. + */ +static bool +match_tags_helper(const char *pattern, const char *tags[]) +{ + const char **tag; + + if (!tags) + return (false); + + for (tag = tags; *tag && **tag != '\0'; tag++) { + if (!fnmatch(pattern, *tag, 0)) + return (true); + } + + return (false); +} + +/* + * Match tags. + * + * Matches against test case tags apply to all the test + * functions in the test case. + * + * Matches against test function tags apply to the matched + * test function only. + */ +static void +match_tags(struct selection_option *option, + struct test_case_selector *tcs) +{ + const struct test_case_descriptor *tcd; + const struct test_function_descriptor *tfd; + struct test_function_selector *tfs; + + tcd = tcs->tcs_descriptor; + + /* + * If the tag in the option matches a tag associated with + * a test case, then we set all of the test case's functions + * to the specified selection state. + */ + if (match_tags_helper(option->so_pattern, tcd->tc_tags)) { + STAILQ_FOREACH(tfs, &tcs->tcs_functions, tfs_next) + tfs->tfs_is_selected = option->so_select_tests; + return; + } + + /* + * Otherwise, check the tag against the tags for each function + * in the test case and set the selection state of each matched + * function. + */ + STAILQ_FOREACH(tfs, &tcs->tcs_functions, tfs_next) { + tfd = tfs->tfs_descriptor; + if (match_tags_helper(option->so_pattern, tfd->tf_tags)) + tfs->tfs_is_selected = option->so_select_tests; + } +} + +/* + * Add the selected tests to the test run. + * + * The memory used by the options list is returned to the system when this + * function completes. + */ +static void +select_tests(struct test_run *tr, + struct selection_option_list *selections) +{ + int i, j; + struct selection_option *selection; + const struct test_case_descriptor *tcd; + struct test_case_selector *tcs; + struct test_function_selector *tfs; + bool default_selection_state; + int selected_count; + + default_selection_state = STAILQ_EMPTY(selections); + + /* + * Set up runtime descriptors. + */ + for (i = 0; i < test_case_count; i++) { + if ((tcs = calloc(1, sizeof(*tcs))) == NULL) + err(EX_OSERR, "cannot allocate a test-case selector"); + STAILQ_INSERT_TAIL(&tr->tr_test_cases, tcs, tcs_next); + STAILQ_INIT(&tcs->tcs_functions); + + tcd = &test_cases[i]; + + tcs->tcs_descriptor = tcd; + + for (j = 0; j < tcd->tc_count; j++) { + if ((tfs = calloc(1, sizeof(*tfs))) == NULL) + err(EX_OSERR, "cannot allocate a test " + "function selector"); + STAILQ_INSERT_TAIL(&tcs->tcs_functions, tfs, tfs_next); + + tfs->tfs_descriptor = tcd->tc_tests + j; + tfs->tfs_is_selected = default_selection_state; + } + } + + /* + * Set or reset the selection state based on the options. + */ + STAILQ_FOREACH(selection, selections, so_next) { + STAILQ_FOREACH(tcs, &tr->tr_test_cases, tcs_next) { + switch (selection->so_selection_scope) { + case SCOPE_TEST_CASE: + match_test_cases(selection, tcs); + break; + case SCOPE_TEST_FUNCTION: + match_test_functions(selection, tcs); + break; + case SCOPE_TAG: + match_tags(selection, tcs); + break; + } + } + } + + /* + * Determine the count of tests selected, for each test case. + */ + STAILQ_FOREACH(tcs, &tr->tr_test_cases, tcs_next) { + selected_count = 0; + STAILQ_FOREACH(tfs, &tcs->tcs_functions, tfs_next) + selected_count += tfs->tfs_is_selected; + tcs->tcs_selected_count = selected_count; + } + + /* Free up the selection list. */ + while (!STAILQ_EMPTY(selections)) { + selection = STAILQ_FIRST(selections); + STAILQ_REMOVE_HEAD(selections, so_next); + free(selection); + } +} + +/* + * Translate a file name to absolute form. + * + * The caller needs to free the returned pointer. + */ +static char * +to_absolute_path(const char *filename) +{ + size_t space_needed; + char *absolute_path; + char current_directory[PATH_MAX]; + + if (filename == NULL || *filename == '\0') + return (NULL); + if (*filename == '/') + return strdup(filename); + + if (getcwd(current_directory, sizeof(current_directory)) == NULL) + err(1, "getcwd failed"); + + /* Reserve space for the slash separator and the trailing NUL. */ + space_needed = strlen(current_directory) + strlen(filename) + 2; + if ((absolute_path = malloc(space_needed)) == NULL) + err(1, "malloc failed"); + if (snprintf(absolute_path, space_needed, "%s/%s", current_directory, + filename) != (int) (space_needed - 1)) + err(1, "snprintf failed"); + return (absolute_path); +} + + +/* + * Display run parameters. + */ + +#define FIELD_NAME_WIDTH 24 +#define INFOLINE(NAME, FLAG, FORMAT, ...) do { \ + printf("I %c %-*s " FORMAT, \ + (FLAG) ? '!' : '.', \ + FIELD_NAME_WIDTH, NAME, __VA_ARGS__); \ + } while (0) + +static void +show_run_header(const struct test_run *tr) +{ + time_t start_time; + struct test_search_path_entry *path_entry; + + if (tr->tr_verbosity == 0) + return; + + INFOLINE("test-run-name", tr->tr_commandline_flags & TRF_NAME, + "%s\n", tr->tr_name); + + INFOLINE("test-execution-style", + tr->tr_commandline_flags & TRF_EXECUTION_STYLE, + "%s\n", to_execution_style_name(tr->tr_style)); + + if (!STAILQ_EMPTY(&tr->tr_search_path)) { + INFOLINE("test-search-path", + tr->tr_commandline_flags & TRF_SEARCH_PATH, + "%c", '['); + STAILQ_FOREACH(path_entry, &tr->tr_search_path, tsp_next) { + printf(" %s", path_entry->tsp_directory); + } + printf(" ]\n"); + } + + INFOLINE("test-run-base-directory", + tr->tr_commandline_flags & TRF_BASE_DIRECTORY, + "%s\n", tr->tr_runtime_base_directory); + + if (tr->tr_artefact_archive) { + INFOLINE("test-artefact-archive", + tr->tr_commandline_flags & TRF_ARTEFACT_ARCHIVE, + "%s\n", tr->tr_artefact_archive); + } + + printf("I %c %-*s ", + tr->tr_commandline_flags & TRF_EXECUTION_TIME ? '=' : '.', + FIELD_NAME_WIDTH, "test-execution-time"); + if (tr->tr_max_seconds_per_test == 0) + printf("unlimited\n"); + else + printf("%lu\n", tr->tr_max_seconds_per_test); + + printf("I %% %-*s %d\n", FIELD_NAME_WIDTH, "test-case-count", + test_case_count); + + if (tr->tr_action == TEST_RUN_EXECUTE) { + start_time = time(NULL); + printf("I %% %-*s %s", FIELD_NAME_WIDTH, + "test-run-start-time", ctime(&start_time)); + } +} + +static void +show_run_trailer(const struct test_run *tr) +{ + time_t end_time; + + if (tr->tr_verbosity == 0) + return; + + if (tr->tr_action == TEST_RUN_EXECUTE) { + end_time = time(NULL); + printf("I %% %-*s %s", FIELD_NAME_WIDTH, "test-run-end-time", + asctime(localtime(&end_time))); + } +} + +#undef INFOLINE +#undef FIELD_HEADER_WIDTH + +/* + * Helper: returns a character indicating the selection status for + * a test case. This character is as follows: + * + * - "*" all test functions in the test case were selected. + * - "+" some test functions in the test case were selected. + * - "-" no test functions from the test case were selected. + */ +static int +get_test_case_status(const struct test_case_selector *tcs) +{ + if (tcs->tcs_selected_count == 0) + return '-'; + if (tcs->tcs_selected_count == tcs->tcs_descriptor->tc_count) + return '*'; + return '?'; +} + +/* + * Helper: print out a comma-separated list of tags. + */ +static void +show_tags(int indent, const char *tags[]) +{ + const char **tag; + + printf("%*c: ", indent, ' '); + for (tag = tags; *tag && **tag != '\0';) { + printf("%s", *tag++); + if (*tag && **tag != '\0') + printf(","); + } + printf("\n"); +} + +/* + * Display a test case descriptor. + */ +static void +show_test_case(struct test_run *tr, const struct test_case_selector *tcs) +{ + const struct test_case_descriptor *tcd; + int prefix_char; + + prefix_char = get_test_case_status(tcs); + tcd = tcs->tcs_descriptor; + + printf("C %c %s\n", prefix_char, tcd->tc_name); + + if (tr->tr_verbosity > 0 && tcd->tc_tags != NULL) + show_tags(2, tcd->tc_tags); + + if (tr->tr_verbosity > 1 && tcd->tc_description) + printf(" & %s\n", tcd->tc_description); +} + +static void +show_test_function(struct test_run *tr, + const struct test_function_selector *tfs) +{ + const struct test_function_descriptor *tfd; + int selection_char; + + selection_char = tfs->tfs_is_selected ? '*' : '-'; + tfd = tfs->tfs_descriptor; + + printf(" F %c %s\n", selection_char, tfd->tf_name); + + if (tr->tr_verbosity > 0 && tfd->tf_tags != NULL) + show_tags(4, tfd->tf_tags); + + if (tr->tr_verbosity > 1 && tfd->tf_description) + printf(" & %s\n", tfd->tf_description); +} + +static int +show_listing(struct test_run *tr) +{ + const struct test_case_selector *tcs; + const struct test_function_selector *tfs; + + STAILQ_FOREACH(tcs, &tr->tr_test_cases, tcs_next) { + show_test_case(tr, tcs); + STAILQ_FOREACH(tfs, &tcs->tcs_functions, tfs_next) + show_test_function(tr, tfs); + } + + return (EXIT_SUCCESS); +} + +int +main(int argc, char **argv) +{ + struct test_run *tr; + int exit_code, option; + enum test_run_style run_style; + struct selection_option *selector; + struct selection_option_list selections = + STAILQ_HEAD_INITIALIZER(selections); + + tr = test_driver_allocate_run(); + + /* Parse arguments. */ + while ((option = getopt(argc, argv, ":R:T:c:ln:p:s:t:v")) != -1) { + switch (option) { + case 'R': /* Test runtime directory. */ + if (!test_driver_is_directory(optarg)) + errx(EX_USAGE, "option -%c: argument \"%s\" " + "does not name a directory.", option, + optarg); + tr->tr_runtime_base_directory = realpath(optarg, NULL); + if (tr->tr_runtime_base_directory == NULL) + err(1, "realpath failed for \"%s\"", optarg); + tr->tr_commandline_flags |= TRF_BASE_DIRECTORY; + break; + case 'T': /* Max execution time for a test function. */ + if (!parse_execution_time( + optarg, &tr->tr_max_seconds_per_test)) + errx(EX_USAGE, "option -%c: argument \"%s\" " + "is not a valid execution time value.", + option, optarg); + tr->tr_commandline_flags |= TRF_EXECUTION_TIME; + break; + case 'c': /* The archive holding artefacts. */ + tr->tr_artefact_archive = to_absolute_path(optarg); + tr->tr_commandline_flags |= TRF_ARTEFACT_ARCHIVE; + break; + case 'l': /* List matching tests. */ + tr->tr_action = TEST_RUN_LIST; + break; + case 'n': /* Test run name. */ + if (tr->tr_name) + free(tr->tr_name); + tr->tr_name = strdup(optarg); + tr->tr_commandline_flags |= TRF_NAME; + break; + case 'p': /* Add a search path entry. */ + if (!test_driver_add_search_path(tr, optarg)) + errx(EX_USAGE, "option -%c: argument \"%s\" " + "does not name a directory.", option, + optarg); + tr->tr_commandline_flags |= TRF_SEARCH_PATH; + break; + case 's': /* Test execution style. */ + if (!parse_run_style(optarg, &run_style)) + errx(EX_USAGE, "option -%c: argument \"%s\" " + "is not a supported test execution style.", + option, optarg); + tr->tr_style = run_style; + tr->tr_commandline_flags |= TRF_EXECUTION_STYLE; + break; + case 't': /* Test selection option. */ + if ((selector = parse_selection_option(optarg)) == NULL) + errx(EX_USAGE, "option -%c: argument \"%s\" " + "is not a valid selection pattern.", + option, optarg); + STAILQ_INSERT_TAIL(&selections, selector, so_next); + break; + case 'v': + tr->tr_verbosity++; + break; + case ':': + errx(EX_USAGE, + "ERROR: option -%c requires an argument.", optopt); + break; + case '?': + errx(EX_USAGE, + "ERROR: unrecognized option -%c", optopt); + break; + default: + errx(EX_USAGE, "ERROR: unspecified error."); + break; + } + } + + /* + * Set unset fields of the test run descriptor to their + * defaults. + */ + if (!test_driver_finish_run_initialization(tr, argv[0])) + err(EX_OSERR, "cannot initialize test driver"); + + /* Choose tests and test cases to act upon. */ + select_tests(tr, &selections); + + assert(STAILQ_EMPTY(&selections)); + + show_run_header(tr); + + /* Perform the requested action. */ + switch (tr->tr_action) { + case TEST_RUN_LIST: + exit_code = show_listing(tr); + break; + + case TEST_RUN_EXECUTE: + default: + /* Not yet implemented. */ + exit_code = EX_UNAVAILABLE; + } + + show_run_trailer(tr); + + test_driver_free_run(tr); + + exit(exit_code); +} |