blob: f7be817d88c4b3a234252854a6b8c22527c8c8c6 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <errno.h>
#include <inttypes.h>
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <algorithm>
#include <string_view>
#include <utility>
#include <fbl/string.h>
#include <fbl/vector.h>
#include <runtests-utils/runtests-utils.h>
namespace runtests {
namespace {
// Ignore test directories where the last component is this. This permits users
// to specify a more general glob that might match to a subdirectory
// containing data for a particular test, which would result in failure should
// runtests try to enter it.
constexpr char kIgnoreDirName[] = "helper";
int Usage(const char* name, const fbl::Vector<fbl::String>& default_test_dirs) {
fprintf(stderr,
"Usage: %s [-S|-s] [-M|-m] [-L|-l] [--all] \n"
" [--names|-n test names] [--output|-o directory] \n"
" [test paths or URLs ...] \n"
" [-- [-args -to -the -test -bins]] \n"
" \n"
"After tests, `--` can be followed by any \n"
"number of arguments to pass to all of the binaries under \n"
"test. \n"
" \n"
"After test is run, a signature of [runtests][PASSED] \n"
"or [runtests][FAILED] will be printed \n"
" \n"
"If --all or --names|-n is passed, tests will be run from \n"
"the default globs: \n",
name);
for (const auto& test_dir : default_test_dirs) {
fprintf(stderr, "\t%s\n", test_dir.c_str());
}
fprintf(stderr,
" \n"
"options: \n"
" -h: See this message \n"
" -d: Dry run, just print test file names and exit \n"
" -i: Per-test timeout in seconds. [2] \n"
" -r: Repeat the test suite this many times \n"
" --names: Filter tests found in the default directory \n"
" globs by these basenames. Also accepts \n"
" fuchsia-pkg URIs, which are run regardless \n"
" of directory globs. (accepts a \n"
" comma-separated list) \n"
" -n: Same as --names. \n"
" --output: Write test summary to a directory [3] \n"
" -o: Same as --output. \n"
" --all: Run tests found in the default directory \n"
" globs. \n"
" \n"
"[1] The test selection options -[sSmMlL] only work \n"
" for tests that support the RUNTESTS_TEST_CLASS \n"
" environment variable. \n"
" \n"
"[2] Will consider tests failed if they don't \n"
" finish in this time. \n"
" \n"
"[3] If -o is enabled, then a JSON summary of the test \n"
" results will be written to a file named \n"
" \"summary.json\" under that directory. \n"
" Any data sink files referenced in the summary will \n"
" also be written to that directory. \n"
" For details, see \n"
" zircon/system/ulib/runtests-utils/summary-schema.json\n");
return EXIT_FAILURE;
}
} // namespace
int DiscoverAndRunTests(int argc, const char* const* argv,
const fbl::Vector<fbl::String>& default_test_dirs, Stopwatch* stopwatch,
const std::string_view syslog_file_name) {
bool use_default_globs = false;
fbl::Vector<fbl::String> basename_whitelist;
fbl::Vector<fbl::String> test_args;
fbl::Vector<fbl::String> test_paths;
const char* output_dir = nullptr;
unsigned int timeout_seconds = 0;
int repeat = 1;
bool dry_run = false;
int optind = 1;
while (optind < argc) {
// Implementing our own opt parsing here is less effort than fixing up
// the behavior across three different getopt implementations on macos,
// linux and zircon, even with this comment. The breaking requirement is
// to parse globs at any position in argv.
fbl::String arg(argv[optind++]);
if (arg.length() == 0) {
continue;
}
if (arg == "--") {
for (; optind < argc; ++optind) {
test_args.push_back(argv[optind]);
}
break;
}
if (arg == "--all" || arg == "--names" || arg == "-n") {
use_default_globs = true;
if (arg == "--names" || arg == "-n") {
if (optind >= argc) {
fprintf(stderr, "Missing argument for %s\n", arg.c_str());
return EXIT_FAILURE;
}
ParseTestNames(argv[optind++], &basename_whitelist);
}
continue;
}
if (arg == "--output" || arg == "-o") {
if (optind >= argc) {
fprintf(stderr, "Missing argument for %s\n", arg.c_str());
return EXIT_FAILURE;
}
output_dir = argv[optind++];
continue;
}
if (arg.data()[0] != '-') {
test_paths.push_back(std::move(arg));
continue;
}
switch (arg.data()[1]) {
case 'h':
return Usage(argv[0], default_test_dirs);
case 'r': {
if (optind >= argc) {
fprintf(stderr, "Missing argument for -r\n");
return EXIT_FAILURE;
}
const char* repeat_str = argv[optind++];
char* end;
long repeatl = strtol(repeat_str, &end, 0);
if (*repeat_str == '\0' || *end != '\0' || repeatl < 0 || repeatl > INT_MAX) {
fprintf(stderr, "Error: bad repeat\n");
return EXIT_FAILURE;
}
repeat = static_cast<int>(repeatl);
break;
}
case 'i': {
if (optind >= argc) {
fprintf(stderr, "Missing argument for %s\n", arg.data());
return EXIT_FAILURE;
}
const char* timeout_str = argv[optind++];
char* end;
long timeout = strtol(timeout_str, &end, 0);
if (*timeout_str == '\0' || *end != '\0' || timeout < 0 || timeout > INT_MAX) {
fprintf(stderr, "Error: bad timeout\n");
return EXIT_FAILURE;
}
timeout_seconds = static_cast<unsigned int>(timeout);
break;
}
case 'd': {
dry_run = true;
break;
}
default:
return Usage(argv[0], default_test_dirs);
}
}
if (use_default_globs) {
const int err =
DiscoverTestsInDirGlobs(default_test_dirs, kIgnoreDirName, basename_whitelist, &test_paths);
if (err) {
fprintf(stderr, "Failed to find tests in dirs: %s\n", strerror(err));
return EXIT_FAILURE;
}
}
if (test_paths.is_empty()) {
fprintf(stderr, "No tests found or specified.\n");
return EXIT_FAILURE;
}
struct stat st;
if (output_dir != nullptr && stat(output_dir, &st) < 0 && (st.st_mode & S_IFMT) == S_IFDIR) {
fprintf(stderr, "Error: Could not open %s\n", output_dir);
return EXIT_FAILURE;
}
if (dry_run) {
printf("Would run the following tests:\n");
for (const auto& test_path : test_paths) {
printf("\t%s\n", test_path.c_str());
}
return EXIT_SUCCESS;
}
std::sort(test_paths.begin(), test_paths.end());
stopwatch->Start();
int failed_count = 0;
fbl::Vector<std::unique_ptr<Result>> results;
if (!RunTests(test_paths, test_args, repeat,
static_cast<int64_t>(timeout_seconds) * static_cast<int64_t>(1000), output_dir,
&failed_count, &results)) {
return EXIT_FAILURE;
}
if (output_dir != nullptr) {
char summary_path[PATH_MAX];
snprintf(summary_path, sizeof(summary_path), "%s/summary.json", output_dir);
FILE* summary_json = fopen(summary_path, "w");
if (summary_json == nullptr) {
fprintf(stderr, "Error: Could not open JSON summary file.\n");
return EXIT_FAILURE;
}
const int error = WriteSummaryJSON(results, syslog_file_name, summary_json);
if (error) {
fprintf(stderr, "Error: Failed to write JSON summary: %s\n", strerror(error));
return EXIT_FAILURE;
}
if (fclose(summary_json)) {
fprintf(stderr, "Error: Could not close JSON summary.\n");
return EXIT_FAILURE;
}
}
if (results.is_empty()) {
printf("\nWARNING: 0 tests run.\n");
} else if (results.size() > 1) {
// In the the case of a single test, this information is already present in
// the last line of output.
if (failed_count) {
printf("\nThe following tests failed:\n");
}
for (const std::unique_ptr<Result>& result : results) {
switch (result->launch_status) {
case SUCCESS:
break;
case FAILED_TO_LAUNCH:
printf("%s: failed to launch\n", result->name.c_str());
break;
case FAILED_TO_WAIT:
printf("%s: failed to wait\n", result->name.c_str());
break;
case FAILED_TO_RETURN_CODE:
printf("%s: failed to return exit code\n", result->name.c_str());
break;
case FAILED_NONZERO_RETURN_CODE:
printf("%s: returned nonzero: %" PRId64 "\n", result->name.c_str(), result->return_code);
break;
case TIMED_OUT:
printf("%s: timed out\n", result->name.c_str());
break;
default:
printf("%s: unknown result\n", result->name.c_str());
break;
}
}
// TODO(https://fxbug.dev/42106940): Include total duration in summary.json.
uint64_t time_taken_ms = stopwatch->DurationInMsecs();
printf("\nSUMMARY: Ran %lu tests: %d failed (%" PRIu64 ".%03u sec)\n", results.size(),
failed_count, time_taken_ms / 1000, (unsigned)(time_taken_ms % 1000));
}
return failed_count ? EXIT_FAILURE : EXIT_SUCCESS;
}
} // namespace runtests