blob: 1b671bf1394e049045adcac00674c93d122efc42 [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 <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/memfs/memfs.h>
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <memory>
#include <fbl/string_buffer.h>
#include <fbl/unique_fd.h>
#include <fbl/vector.h>
#include <re2/re2.h>
#include <runtests-utils/runtests-utils.h>
#include <zxtest/zxtest.h>
#include "runtests-utils-test-utils.h"
namespace runtests {
namespace {
static constexpr size_t kOneMegabyte = 1 << 20;
TEST(ParseTestNames, ParseTestNamesEmptyStr) {
fbl::String input("");
fbl::Vector<fbl::String> parsed;
ParseTestNames(input, &parsed);
EXPECT_EQ(0, parsed.size());
}
TEST(ParseTestNames, ParseTestNamesEmptyStrInMiddle) {
fbl::String input("a,,b");
fbl::Vector<fbl::String> parsed;
ParseTestNames(input, &parsed);
ASSERT_EQ(2, parsed.size());
EXPECT_STR_EQ("a", parsed[0].c_str());
EXPECT_STR_EQ("b", parsed[1].c_str());
}
TEST(ParseTestNames, ParseTestNamesTrailingComma) {
fbl::String input("a,");
fbl::Vector<fbl::String> parsed;
ParseTestNames(input, &parsed);
ASSERT_EQ(1, parsed.size());
EXPECT_STR_EQ("a", parsed[0].c_str());
}
TEST(ParseTestNames, ParseTestNamesNormal) {
fbl::String input("a,b");
fbl::Vector<fbl::String> parsed;
ParseTestNames(input, &parsed);
ASSERT_EQ(2, parsed.size());
EXPECT_STR_EQ("a", parsed[0].c_str());
EXPECT_STR_EQ("b", parsed[1].c_str());
}
TEST(IsInWhitelist, EmptyWhitelist) {
fbl::Vector<fbl::String> whitelist;
EXPECT_FALSE(IsInWhitelist("a", whitelist));
}
TEST(IsInWhitelist, NonemptyWhitelist) {
fbl::Vector<fbl::String> whitelist = {"b", "a"};
EXPECT_TRUE(IsInWhitelist("a", whitelist));
}
TEST(JoinPath, JoinPathNoTrailingSlash) {
EXPECT_STR_EQ("a/b/c/d", JoinPath("a/b", "c/d").c_str());
}
TEST(JoinPath, JoinPathTrailingSlash) { EXPECT_STR_EQ("a/b/c/d", JoinPath("a/b/", "c/d").c_str()); }
TEST(JoinPath, JoinPathAbsoluteChild) {
EXPECT_STR_EQ("a/b/c/d", JoinPath("a/b/", "/c/d").c_str());
}
TEST(MkDirAll, MkDirAllTooLong) {
char too_long[PATH_MAX + 2];
memset(too_long, 'a', PATH_MAX + 1);
too_long[PATH_MAX + 1] = '\0';
EXPECT_EQ(ENAMETOOLONG, MkDirAll(too_long));
}
TEST(MkDirAll, MkDirAllAlreadyExists) {
ScopedTestDir test_dir;
const fbl::String already = JoinPath(test_dir.path(), "already");
const fbl::String exists = JoinPath(already, "exists");
ASSERT_EQ(0, mkdir(already.c_str(), 0755));
ASSERT_EQ(0, mkdir(exists.c_str(), 0755));
EXPECT_EQ(0, MkDirAll(exists));
}
TEST(MkDirAll, MkDirAllParentAlreadyExists) {
ScopedTestDir test_dir;
const fbl::String parent = JoinPath(test_dir.path(), "existing-parent");
const fbl::String child = JoinPath(parent, "child");
ASSERT_EQ(0, mkdir(parent.c_str(), 0755));
EXPECT_EQ(0, MkDirAll(child));
struct stat s;
EXPECT_EQ(0, stat(child.c_str(), &s));
}
TEST(MkDirAll, MkDirAllParentDoesNotExist) {
ScopedTestDir test_dir;
const fbl::String parent = JoinPath(test_dir.path(), "not-existing-parent");
const fbl::String child = JoinPath(parent, "child");
struct stat s;
ASSERT_NE(0, stat(parent.c_str(), &s));
EXPECT_EQ(0, MkDirAll(child));
EXPECT_EQ(0, stat(child.c_str(), &s));
}
TEST(WriteSummaryJSON, WriteSummaryJSONSucceeds) {
// A reasonable guess that the function won't output more than this.
std::unique_ptr<char[]> buf(new char[kOneMegabyte]);
FILE* buf_file = fmemopen(buf.get(), kOneMegabyte, "w");
fbl::Vector<std::unique_ptr<Result>> results;
results.push_back(std::make_unique<Result>("/a", SUCCESS, 0, 10));
results.push_back(std::make_unique<Result>("b", FAILED_TO_LAUNCH, 0, 0));
ASSERT_EQ(0, WriteSummaryJSON(results, "/tmp/file_path", buf_file));
fclose(buf_file);
// We don't have a JSON parser in zircon right now, so just hard-code the
// expected output.
const char kExpectedJSONOutput[] = R"({
"tests": [
{
"name": "/a",
"result": "PASS",
"duration_milliseconds": 10
},
{
"name": "b",
"result": "FAIL",
"duration_milliseconds": 0
}
],
"outputs": {
"syslog_file": "/tmp/file_path"
}
}
)";
EXPECT_STR_EQ(kExpectedJSONOutput, buf.get());
}
TEST(WriteSummaryJSON, WriteSummaryJSONSucceedsWithoutSyslogPath) {
// A reasonable guess that the function won't output more than this.
std::unique_ptr<char[]> buf(new char[kOneMegabyte]);
FILE* buf_file = fmemopen(buf.get(), kOneMegabyte, "w");
fbl::Vector<std::unique_ptr<Result>> results;
results.push_back(std::make_unique<Result>("/a", SUCCESS, 0, 10));
results.push_back(std::make_unique<Result>("b", FAILED_TO_LAUNCH, 0, 0));
ASSERT_EQ(0, WriteSummaryJSON(results, /*syslog_path=*/"", buf_file));
fclose(buf_file);
// With an empty syslog_path, we expect no values under "outputs" and
// "syslog_file" to be generated in the JSON output.
const char kExpectedJSONOutput[] = R"({
"tests": [
{
"name": "/a",
"result": "PASS",
"duration_milliseconds": 10
},
{
"name": "b",
"result": "FAIL",
"duration_milliseconds": 0
}
]
}
)";
EXPECT_STR_EQ(kExpectedJSONOutput, buf.get());
}
TEST(ResolveGlobs, ResolveGlobsNoMatches) {
ScopedTestDir test_dir;
fbl::Vector<fbl::String> resolved;
fbl::String test_fs_glob = JoinPath(test_dir.path(), "bar*");
const fbl::Vector<fbl::String> globs = {"/foo/bar/*", test_fs_glob};
ASSERT_EQ(0, ResolveGlobs(globs, &resolved));
EXPECT_EQ(0, resolved.size());
}
TEST(ResolveGlobs, ResolveGlobsMultipleMatches) {
ScopedTestDir test_dir;
fbl::String existing_dir_path = JoinPath(test_dir.path(), "existing-dir/prefix-suffix");
fbl::String existing_file_path = JoinPath(test_dir.path(), "existing-file");
fbl::String existing_dir_glob = JoinPath(test_dir.path(), "existing-dir/prefix*");
const fbl::Vector<fbl::String> globs = {"/does/not/exist/*",
existing_dir_glob, // matches existing_dir_path.
existing_file_path};
ASSERT_EQ(0, MkDirAll(existing_dir_path));
const int existing_file_fd = open(existing_file_path.c_str(), O_CREAT);
ASSERT_NE(-1, existing_file_fd, "%s", strerror(errno));
ASSERT_NE(-1, close(existing_file_fd), "%s", strerror(errno));
fbl::Vector<fbl::String> resolved;
ASSERT_EQ(0, ResolveGlobs(globs, &resolved));
ASSERT_EQ(2, resolved.size());
EXPECT_STR_EQ(existing_dir_path.c_str(), resolved[0].c_str());
}
TEST(RunTest, RunTestSuccess) {
PackagedScriptFile script_file("succeed.sh");
fbl::String test_name = script_file.path();
const char* argv[] = {test_name.c_str(), nullptr};
std::unique_ptr<Result> result = RunTest(argv, nullptr, test_name.c_str(), 0, nullptr);
EXPECT_STR_EQ(argv[0], result->name.c_str());
EXPECT_EQ(SUCCESS, result->launch_status);
EXPECT_EQ(0, result->return_code);
}
TEST(RunTest, RunTestTimeout) {
// Test timeout is enforced if the test runs too long.
PackagedScriptFile inf_loop_file("test-inf-loop.sh");
fbl::String inf_loop_name = inf_loop_file.path();
const char* inf_loop_argv[] = {inf_loop_name.c_str(), nullptr};
std::unique_ptr<Result> result =
RunTest(inf_loop_argv, nullptr, inf_loop_name.c_str(), 1, nullptr);
EXPECT_STR_EQ(inf_loop_argv[0], result->name.c_str());
EXPECT_EQ(TIMED_OUT, result->launch_status);
EXPECT_EQ(0, result->return_code);
// Test timeout is not enforced if the test finishes quickly.
PackagedScriptFile success_file("succeed.sh");
fbl::String succeed_name = success_file.path();
const char* succeed_argv[] = {succeed_name.c_str(), nullptr};
result = RunTest(succeed_argv, nullptr, succeed_name.c_str(), 100000, nullptr);
EXPECT_STR_EQ(succeed_argv[0], result->name.c_str());
EXPECT_EQ(SUCCESS, result->launch_status);
EXPECT_EQ(0, result->return_code);
}
TEST(RunTest, RunTestFailure) {
ScopedTestDir test_dir;
PackagedScriptFile script_file("expect-this-failure.sh");
fbl::String test_name = script_file.path();
const char* argv[] = {test_name.c_str(), nullptr};
std::unique_ptr<Result> result = RunTest(argv, nullptr, test_name.c_str(), 0, nullptr);
EXPECT_STR_EQ(argv[0], result->name.c_str());
EXPECT_EQ(FAILED_NONZERO_RETURN_CODE, result->launch_status);
EXPECT_EQ(77, result->return_code);
}
TEST(RunTest, RunTestFailureToLoadFile) {
const char* argv[] = {"i/do/not/exist/", nullptr};
std::unique_ptr<Result> result = RunTest(argv, nullptr, argv[0], 0, nullptr);
EXPECT_STR_EQ(argv[0], result->name.c_str());
EXPECT_EQ(FAILED_TO_LAUNCH, result->launch_status);
}
TEST(DiscoverTestsInDirGlobs, DiscoverTestsInDirGlobsBasic) {
ScopedTestDir test_dir;
const fbl::String a_file_name = JoinPath(test_dir.path(), "a.sh");
const fbl::String b_file_name = JoinPath(test_dir.path(), "b.sh");
ScopedStubFile a_file(a_file_name);
ScopedStubFile b_file(b_file_name);
fbl::Vector<fbl::String> discovered_paths;
EXPECT_EQ(0, DiscoverTestsInDirGlobs({test_dir.path()}, nullptr, {}, &discovered_paths));
EXPECT_EQ(2, discovered_paths.size());
bool discovered_a = false;
bool discovered_b = false;
// The order of the results is not defined, so just check that each is
// present.
for (const auto& path : discovered_paths) {
if (path == a_file_name) {
discovered_a = true;
} else if (path == b_file_name) {
discovered_b = true;
}
}
EXPECT_TRUE(discovered_a);
EXPECT_TRUE(discovered_b);
}
TEST(DiscoverTestsInDirGlobs, DiscoverTestsInDirGlobsFilter) {
ScopedTestDir test_dir;
const char kHopefullyUniqueFileBasename[] = "e829cea9919fe045ca199945db7ac99a";
const fbl::String unique_file_name = JoinPath(test_dir.path(), kHopefullyUniqueFileBasename);
ScopedStubFile unique_file(unique_file_name);
// This one should be ignored because its basename is not in the include list.
const fbl::String other_file_name = JoinPath(test_dir.path(), "foo.sh");
ScopedStubFile fail_file(other_file_name);
fbl::Vector<fbl::String> discovered_paths;
EXPECT_EQ(0, DiscoverTestsInDirGlobs({JoinPath(kMemFsRoot, "*")}, nullptr,
{kHopefullyUniqueFileBasename}, &discovered_paths));
EXPECT_EQ(1, discovered_paths.size());
EXPECT_STR_EQ(unique_file_name.c_str(), discovered_paths[0].c_str());
}
TEST(DiscoverTestsInDirGlobs, DiscoverTestsInDirGlobsIgnore) {
ScopedTestDir test_dir_a, test_dir_b;
const fbl::String a_name = JoinPath(test_dir_a.path(), "foo.sh");
ScopedStubFile a_file(a_name);
const fbl::String b_name = JoinPath(test_dir_b.path(), "foo.sh");
ScopedStubFile fail_file(b_name);
fbl::Vector<fbl::String> discovered_paths;
EXPECT_EQ(0, DiscoverTestsInDirGlobs({test_dir_a.path(), test_dir_b.path()},
test_dir_b.basename(), {}, &discovered_paths));
EXPECT_EQ(1, discovered_paths.size());
EXPECT_STR_EQ(a_name.c_str(), discovered_paths[0].c_str());
}
TEST(RunTests, RunTestsWithArguments) {
ScopedTestDir test_dir;
PackagedScriptFile succeed_script("succeed-with-echo.sh");
const fbl::String succeed_file_name = succeed_script.path();
int num_failed = 0;
fbl::Vector<std::unique_ptr<Result>> results;
fbl::Vector<fbl::String> args{"first", "second", "third", "-4", "--", "-", "seventh"};
const fbl::String output_dir = JoinPath(test_dir.path(), "output");
ASSERT_EQ(0, MkDirAll(output_dir));
EXPECT_TRUE(RunTests({succeed_file_name}, args, 1, 0, output_dir.c_str(), nullptr, &num_failed,
&results));
EXPECT_EQ(0, num_failed);
EXPECT_EQ(1, results.size());
}
TEST(DiscoverAndRunTests, DiscoverAndRunTestsBasicPass) {
// The build templates assemble two tests, a.sh and b.sh, in the
// runtestsbasicpass/ subdirectory.
const fbl::String script_dir = packaged_script_dir();
const fbl::String test_script = JoinPath(script_dir, "runtestsbasicpass/a.sh");
const char* const argv[] = {"./runtests", test_script.c_str()};
TestStopwatch stopwatch;
EXPECT_EQ(EXIT_SUCCESS, DiscoverAndRunTests(2, argv, {}, &stopwatch, ""));
}
TEST(DiscoverAndRunTests, DiscoverAndRunTestsBasicFail) {
// The build templates assemble two tests, test-basic-succeed.sh and
// test-basic-fail.sh, in the runtestsbasicfail/ subdirectory.
const fbl::String script_dir = packaged_script_dir();
const fbl::String test_script = JoinPath(script_dir, "runtestsbasicfail/test-basic-fail.sh");
const char* const argv[] = {"./runtests", test_script.c_str()};
TestStopwatch stopwatch;
EXPECT_EQ(EXIT_FAILURE, DiscoverAndRunTests(2, argv, {}, &stopwatch, ""));
}
TEST(DiscoverAndRunTests, DiscoverAndRunTestsFailsWithNoTestOrDefaultDirs) {
ScopedTestDir test_dir;
const fbl::String succeed_file_name = JoinPath(test_dir.path(), "succeed.sh");
ScopedStubFile succeed_file(succeed_file_name);
const char* const argv[] = {"./runtests"};
TestStopwatch stopwatch;
EXPECT_EQ(EXIT_FAILURE, DiscoverAndRunTests(1, argv, {}, &stopwatch, ""));
}
TEST(DiscoverAndRunTests, DiscoverAndRunTestsFailsWithBadArgs) {
fbl::String script_dir = packaged_script_dir();
const char* const argv[] = {"./runtests", "-?", "unknown-arg", script_dir.c_str()};
TestStopwatch stopwatch;
EXPECT_EQ(EXIT_FAILURE, DiscoverAndRunTests(4, argv, {}, &stopwatch, ""));
}
TEST(DiscoverAndRunTests, DiscoverAndRunTestsWithDefaultGlobs) {
// There are three scripts generated in a directory by the build templates:
//
// testglobs/test-globs-root.sh
// testglobs/A/B/C/test-globs-one.sh
// testglobs/A/D/C/test-globs-two.sh
//
// Verify that we find and run all three.
fbl::String all_scripts_dir = packaged_script_dir();
fbl::String script_dir = JoinPath(all_scripts_dir, "testglobs");
fbl::String glob = JoinPath(script_dir, "A/*/C");
const char* const argv[] = {"./runtests", "--all"};
TestStopwatch stopwatch;
EXPECT_EQ(EXIT_SUCCESS,
DiscoverAndRunTests(2, argv, {script_dir.c_str(), glob.c_str()}, &stopwatch, ""));
}
// Passing an --output argument should result in output being written to that
// location.
TEST(DiscoverAndRunTests, DiscoverAndRunTestsWithOutput) {
fbl::String all_scripts_dir = packaged_script_dir();
fbl::String script_dir = JoinPath(all_scripts_dir, "testwithoutput");
ScopedTestDir test_dir;
const fbl::String succeed_file_name = JoinPath(script_dir, "test-with-output-succeed.sh");
const fbl::String fail_file_name = JoinPath(script_dir, "test-with-output-fail.sh");
const fbl::String output_dir = JoinPath(test_dir.path(), "run-all-tests-output-1");
EXPECT_EQ(0, MkDirAll(output_dir));
const char* const argv[] = {"./runtests", "--all", "--output", output_dir.c_str()};
TestStopwatch stopwatch;
EXPECT_EQ(EXIT_FAILURE, DiscoverAndRunTests(4, argv, {script_dir.c_str()}, &stopwatch, ""));
// Prepare the expected output.
char expected_pass_output_buf[1024];
sprintf(expected_pass_output_buf,
R"( \{
"name": "%s",
"result": "PASS",
"duration_milliseconds": \d+
\})",
succeed_file_name.c_str());
re2::RE2 expected_pass_output_regex(expected_pass_output_buf);
char expected_fail_output_buf[1024];
sprintf(expected_fail_output_buf,
R"( \{
"name": "%s",
"result": "FAIL",
"duration_milliseconds": \d+
\})",
fail_file_name.c_str());
re2::RE2 expected_fail_output_regex(expected_fail_output_buf);
// Extract the actual output.
const fbl::String output_path = JoinPath(output_dir, "summary.json");
FILE* output_file = fopen(output_path.c_str(), "r");
ASSERT_TRUE(output_file);
char buf[1024];
memset(buf, 0, sizeof(buf));
EXPECT_LT(0, fread(buf, sizeof(buf[0]), sizeof(buf), output_file));
fclose(output_file);
// The order of the tests in summary.json is not defined, so first check the
// prefix, then be permissive about order of the actual tests.
EXPECT_EQ(0, strncmp(kExpectedJSONOutputPrefix, buf, kExpectedJSONOutputPrefixSize));
re2::StringPiece buf_for_pass(buf);
re2::StringPiece buf_for_fail(buf);
EXPECT_TRUE(re2::RE2::FindAndConsume(&buf_for_pass, expected_pass_output_regex));
EXPECT_TRUE(re2::RE2::FindAndConsume(&buf_for_fail, expected_fail_output_regex));
auto outputs_end = buf_for_pass.length() < buf_for_fail.length() ? buf_for_pass : buf_for_fail;
EXPECT_STR_EQ("\n ]\n}\n", outputs_end.data());
}
// Passing an --output argument *and* a syslog file name should result in output being
// written that includes a syslog reference.
TEST(DiscoverAndRunTests, DiscoverAndRunTestsWithSyslogOutput) {
fbl::String all_scripts_dir = packaged_script_dir();
fbl::String script_dir = JoinPath(all_scripts_dir, "testwithoutput");
ScopedTestDir test_dir;
const fbl::String succeed_file_name = JoinPath(script_dir, "test-with-output-succeed.sh");
const fbl::String fail_file_name = JoinPath(script_dir, "test-with-output-fail.sh");
const fbl::String output_dir = JoinPath(test_dir.path(), "run-all-tests-output-2");
EXPECT_EQ(0, MkDirAll(output_dir));
const char* const argv[] = {"./runtests", "--all", "--output", output_dir.c_str()};
TestStopwatch stopwatch;
EXPECT_EQ(EXIT_FAILURE,
DiscoverAndRunTests(4, argv, {script_dir.c_str()}, &stopwatch, "syslog.txt"));
// Prepare the expected output.
const char kExpectedOutputsStr[] =
" \"outputs\": {\n"
" \"syslog_file\": \"syslog.txt\"\n"
" }";
// Extract the actual output.
const fbl::String output_path = JoinPath(output_dir, "summary.json");
FILE* output_file = fopen(output_path.c_str(), "r");
ASSERT_TRUE(output_file);
char buf[1024];
memset(buf, 0, sizeof(buf));
EXPECT_LT(0, fread(buf, sizeof(buf[0]), sizeof(buf), output_file));
fclose(output_file);
// We don't actually care if the string is at the beginning or the end of
// the JSON, so just search for it anywhere.
bool found_expected_outputs_str = false;
for (size_t buf_index = 0; buf[buf_index]; ++buf_index) {
if (!strncmp(kExpectedOutputsStr, &buf[buf_index], sizeof(kExpectedOutputsStr) - 1)) {
found_expected_outputs_str = true;
break;
}
}
if (!found_expected_outputs_str) {
printf("Unexpected buffer contents: %s\n", buf);
}
EXPECT_TRUE(found_expected_outputs_str, "Didn't find expected outputs str in buf");
}
} // namespace
} // namespace runtests
int main(int argc, char** argv) {
int result;
memfs_filesystem_t* fs;
{
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
if (loop.StartThread() != ZX_OK) {
fprintf(stderr, "Error: Cannot initialize local memfs loop\n");
return -1;
}
if (memfs_install_at(loop.dispatcher(), runtests::kMemFsRoot, &fs) != ZX_OK) {
fprintf(stderr, "Error: Cannot install local memfs\n");
return -1;
}
result = zxtest::RunAllTests(argc, argv);
loop.Shutdown();
}
memfs_uninstall_unsafe(fs, runtests::kMemFsRoot);
return result;
}