platform_system_core/cli-test/cli-test.cpp

321 lines
9.6 KiB
C++
Raw Normal View History

/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <errno.h>
#include <getopt.h>
#include <inttypes.h>
#include <libgen.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <string>
#include <vector>
#include <android-base/chrono_utils.h>
#include <android-base/file.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include <android-base/test_utils.h>
// Example:
// name: unzip -n
// before: mkdir -p d1/d2
// before: echo b > d1/d2/a.txt
// command: unzip -q -n $FILES/zip/example.zip d1/d2/a.txt && cat d1/d2/a.txt
// expected-stdout:
// b
struct Test {
std::string test_filename;
std::string name;
std::string command;
std::vector<std::string> befores;
std::vector<std::string> afters;
std::string expected_stdout;
std::string expected_stderr;
int exit_status = 0;
};
static const char* g_progname;
static bool g_verbose;
static const char* g_file;
static size_t g_line;
enum Color { kRed, kGreen };
static void Print(Color c, const char* lhs, const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
if (isatty(0)) printf("%s", (c == kRed) ? "\e[31m" : "\e[32m");
printf("%s%s", lhs, isatty(0) ? "\e[0m" : "");
vfprintf(stdout, fmt, ap);
putchar('\n');
va_end(ap);
}
static void Die(int error, const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "%s: ", g_progname);
vfprintf(stderr, fmt, ap);
if (error != 0) fprintf(stderr, ": %s", strerror(error));
fprintf(stderr, "\n");
va_end(ap);
_exit(1);
}
static void V(const char* fmt, ...) {
if (!g_verbose) return;
va_list ap;
va_start(ap, fmt);
fprintf(stderr, " - ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
static void SetField(const char* what, std::string* field, std::string_view value) {
if (!field->empty()) {
Die(0, "%s:%zu: %s already set to '%s'", g_file, g_line, what, field->c_str());
}
field->assign(value);
}
// Similar to ConsumePrefix, but also trims, so "key:value" and "key: value"
// are equivalent.
static bool Match(std::string* s, const std::string& prefix) {
if (!android::base::StartsWith(*s, prefix)) return false;
s->assign(android::base::Trim(s->substr(prefix.length())));
return true;
}
static void CollectTests(std::vector<Test>* tests, const char* test_filename) {
std::string absolute_test_filename;
if (!android::base::Realpath(test_filename, &absolute_test_filename)) {
Die(errno, "realpath '%s'", test_filename);
}
std::string content;
if (!android::base::ReadFileToString(test_filename, &content)) {
Die(errno, "couldn't read '%s'", test_filename);
}
size_t count = 0;
g_file = test_filename;
g_line = 0;
auto lines = android::base::Split(content, "\n");
std::unique_ptr<Test> test(new Test);
while (g_line < lines.size()) {
auto line = lines[g_line++];
if (line.empty() || line[0] == '#') continue;
if (line[0] == '-') {
if (test->name.empty() || test->command.empty()) {
Die(0, "%s:%zu: each test requires both a name and a command", g_file, g_line);
}
test->test_filename = absolute_test_filename;
tests->push_back(*test.release());
test.reset(new Test);
++count;
} else if (Match(&line, "name:")) {
SetField("name", &test->name, line);
} else if (Match(&line, "command:")) {
SetField("command", &test->command, line);
} else if (Match(&line, "before:")) {
test->befores.push_back(line);
} else if (Match(&line, "after:")) {
test->afters.push_back(line);
} else if (Match(&line, "expected-stdout:")) {
// Collect tab-indented lines.
std::string text;
while (g_line < lines.size() && !lines[g_line].empty() && lines[g_line][0] == '\t') {
text += lines[g_line++].substr(1) + "\n";
}
SetField("expected stdout", &test->expected_stdout, text);
} else {
Die(0, "%s:%zu: syntax error: \"%s\"", g_file, g_line, line.c_str());
}
}
if (count == 0) Die(0, "no tests found in '%s'", g_file);
}
static const char* Plural(size_t n) {
return (n == 1) ? "" : "s";
}
static std::string ExitStatusToString(int status) {
if (WIFSIGNALED(status)) {
return android::base::StringPrintf("was killed by signal %d (%s)", WTERMSIG(status),
strsignal(WTERMSIG(status)));
}
if (WIFSTOPPED(status)) {
return android::base::StringPrintf("was stopped by signal %d (%s)", WSTOPSIG(status),
strsignal(WSTOPSIG(status)));
}
return android::base::StringPrintf("exited with status %d", WEXITSTATUS(status));
}
static bool RunCommands(const char* what, const std::vector<std::string>& commands) {
bool result = true;
for (auto& command : commands) {
V("running %s \"%s\"", what, command.c_str());
int exit_status = system(command.c_str());
if (exit_status != 0) {
result = false;
fprintf(stderr, "Command (%s) \"%s\" %s\n", what, command.c_str(),
ExitStatusToString(exit_status).c_str());
}
}
return result;
}
static bool CheckOutput(const char* what, std::string actual_output,
const std::string& expected_output, const std::string& FILES) {
// Rewrite the output to reverse any expansion of $FILES.
actual_output = android::base::StringReplace(actual_output, FILES, "$FILES", true);
bool result = (actual_output == expected_output);
if (!result) {
fprintf(stderr, "Incorrect %s.\nExpected:\n%s\nActual:\n%s\n", what, expected_output.c_str(),
actual_output.c_str());
}
return result;
}
static int RunTests(const std::vector<Test>& tests) {
std::vector<std::string> failures;
Print(kGreen, "[==========]", " Running %zu tests.", tests.size());
android::base::Timer total_timer;
for (const auto& test : tests) {
bool failed = false;
Print(kGreen, "[ RUN ]", " %s", test.name.c_str());
android::base::Timer test_timer;
// Set $FILES for this test.
std::string FILES = android::base::Dirname(test.test_filename) + "/files";
V("setenv(\"FILES\", \"%s\")", FILES.c_str());
setenv("FILES", FILES.c_str(), 1);
// Make a safe space to run the test.
TemporaryDir td;
V("chdir(\"%s\")", td.path);
if (chdir(td.path)) Die(errno, "chdir(\"%s\")", td.path);
// Perform any setup specified for this test.
if (!RunCommands("before", test.befores)) failed = true;
if (!failed) {
V("running command \"%s\"", test.command.c_str());
CapturedStdout test_stdout;
CapturedStderr test_stderr;
int exit_status = system(test.command.c_str());
test_stdout.Stop();
test_stderr.Stop();
V("exit status %d", exit_status);
if (exit_status != test.exit_status) {
failed = true;
fprintf(stderr, "Incorrect exit status: expected %d but %s\n", test.exit_status,
ExitStatusToString(exit_status).c_str());
}
if (!CheckOutput("stdout", test_stdout.str(), test.expected_stdout, FILES)) failed = true;
if (!CheckOutput("stderr", test_stderr.str(), test.expected_stderr, FILES)) failed = true;
if (!RunCommands("after", test.afters)) failed = true;
}
std::stringstream duration;
duration << test_timer;
if (failed) {
failures.push_back(test.name);
Print(kRed, "[ FAILED ]", " %s (%s)", test.name.c_str(), duration.str().c_str());
} else {
Print(kGreen, "[ OK ]", " %s (%s)", test.name.c_str(), duration.str().c_str());
}
}
// Summarize the whole run and explicitly list all the failures.
std::stringstream duration;
duration << total_timer;
Print(kGreen, "[==========]", " %zu tests ran. (%s total)", tests.size(), duration.str().c_str());
size_t fail_count = failures.size();
size_t pass_count = tests.size() - fail_count;
Print(kGreen, "[ PASSED ]", " %zu test%s.", pass_count, Plural(pass_count));
if (!failures.empty()) {
Print(kRed, "[ FAILED ]", " %zu test%s.", fail_count, Plural(fail_count));
for (auto& failure : failures) {
Print(kRed, "[ FAILED ]", " %s", failure.c_str());
}
}
return (fail_count == 0) ? 0 : 1;
}
static void ShowHelp(bool full) {
fprintf(full ? stdout : stderr, "usage: %s [-v] FILE...\n", g_progname);
if (!full) exit(EXIT_FAILURE);
printf(
"\n"
"Run tests.\n"
"\n"
"-v\tVerbose (show workings)\n");
exit(EXIT_SUCCESS);
}
int main(int argc, char* argv[]) {
g_progname = basename(argv[0]);
static const struct option opts[] = {
{"help", no_argument, 0, 'h'},
{"verbose", no_argument, 0, 'v'},
{},
};
int opt;
while ((opt = getopt_long(argc, argv, "hv", opts, nullptr)) != -1) {
switch (opt) {
case 'h':
ShowHelp(true);
break;
case 'v':
g_verbose = true;
break;
default:
ShowHelp(false);
break;
}
}
argv += optind;
if (!*argv) Die(0, "no test files provided");
std::vector<Test> tests;
for (; *argv; ++argv) CollectTests(&tests, *argv);
return RunTests(tests);
}