| Elliott Hughes | f276140 | 2019-11-15 15:07:00 -0800 | [diff] [blame] | 1 | /* | 
|  | 2 | * Copyright (C) 2019 The Android Open Source Project | 
|  | 3 | * | 
|  | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | 5 | * you may not use this file except in compliance with the License. | 
|  | 6 | * You may obtain a copy of the License at | 
|  | 7 | * | 
|  | 8 | *      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 9 | * | 
|  | 10 | * Unless required by applicable law or agreed to in writing, software | 
|  | 11 | * distributed under the License is distributed on an "AS IS" BASIS, | 
|  | 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | 13 | * See the License for the specific language governing permissions and | 
|  | 14 | * limitations under the License. | 
|  | 15 | */ | 
|  | 16 |  | 
|  | 17 | #include <errno.h> | 
|  | 18 | #include <getopt.h> | 
|  | 19 | #include <inttypes.h> | 
|  | 20 | #include <libgen.h> | 
|  | 21 | #include <stdarg.h> | 
|  | 22 | #include <stdio.h> | 
|  | 23 | #include <stdlib.h> | 
|  | 24 | #include <string.h> | 
|  | 25 | #include <sys/wait.h> | 
|  | 26 | #include <time.h> | 
|  | 27 | #include <unistd.h> | 
|  | 28 |  | 
|  | 29 | #include <string> | 
|  | 30 | #include <vector> | 
|  | 31 |  | 
|  | 32 | #include <android-base/chrono_utils.h> | 
|  | 33 | #include <android-base/file.h> | 
|  | 34 | #include <android-base/stringprintf.h> | 
|  | 35 | #include <android-base/strings.h> | 
|  | 36 | #include <android-base/test_utils.h> | 
|  | 37 |  | 
|  | 38 | // Example: | 
|  | 39 |  | 
|  | 40 | // name: unzip -n | 
|  | 41 | // before: mkdir -p d1/d2 | 
|  | 42 | // before: echo b > d1/d2/a.txt | 
|  | 43 | // command: unzip -q -n $FILES/zip/example.zip d1/d2/a.txt && cat d1/d2/a.txt | 
|  | 44 | // expected-stdout: | 
|  | 45 | // 	b | 
|  | 46 |  | 
|  | 47 | struct Test { | 
|  | 48 | std::string test_filename; | 
|  | 49 | std::string name; | 
|  | 50 | std::string command; | 
|  | 51 | std::vector<std::string> befores; | 
|  | 52 | std::vector<std::string> afters; | 
|  | 53 | std::string expected_stdout; | 
|  | 54 | std::string expected_stderr; | 
|  | 55 | int exit_status = 0; | 
|  | 56 | }; | 
|  | 57 |  | 
|  | 58 | static const char* g_progname; | 
|  | 59 | static bool g_verbose; | 
|  | 60 |  | 
|  | 61 | static const char* g_file; | 
|  | 62 | static size_t g_line; | 
|  | 63 |  | 
|  | 64 | enum Color { kRed, kGreen }; | 
|  | 65 |  | 
|  | 66 | static void Print(Color c, const char* lhs, const char* fmt, ...) { | 
|  | 67 | va_list ap; | 
|  | 68 | va_start(ap, fmt); | 
|  | 69 | if (isatty(0)) printf("%s", (c == kRed) ? "\e[31m" : "\e[32m"); | 
|  | 70 | printf("%s%s", lhs, isatty(0) ? "\e[0m" : ""); | 
|  | 71 | vfprintf(stdout, fmt, ap); | 
|  | 72 | putchar('\n'); | 
|  | 73 | va_end(ap); | 
|  | 74 | } | 
|  | 75 |  | 
|  | 76 | static void Die(int error, const char* fmt, ...) { | 
|  | 77 | va_list ap; | 
|  | 78 | va_start(ap, fmt); | 
|  | 79 | fprintf(stderr, "%s: ", g_progname); | 
|  | 80 | vfprintf(stderr, fmt, ap); | 
|  | 81 | if (error != 0) fprintf(stderr, ": %s", strerror(error)); | 
|  | 82 | fprintf(stderr, "\n"); | 
|  | 83 | va_end(ap); | 
|  | 84 | _exit(1); | 
|  | 85 | } | 
|  | 86 |  | 
|  | 87 | static void V(const char* fmt, ...) { | 
|  | 88 | if (!g_verbose) return; | 
|  | 89 |  | 
|  | 90 | va_list ap; | 
|  | 91 | va_start(ap, fmt); | 
|  | 92 | fprintf(stderr, "           - "); | 
|  | 93 | vfprintf(stderr, fmt, ap); | 
|  | 94 | fprintf(stderr, "\n"); | 
|  | 95 | va_end(ap); | 
|  | 96 | } | 
|  | 97 |  | 
|  | 98 | static void SetField(const char* what, std::string* field, std::string_view value) { | 
|  | 99 | if (!field->empty()) { | 
|  | 100 | Die(0, "%s:%zu: %s already set to '%s'", g_file, g_line, what, field->c_str()); | 
|  | 101 | } | 
|  | 102 | field->assign(value); | 
|  | 103 | } | 
|  | 104 |  | 
|  | 105 | // Similar to ConsumePrefix, but also trims, so "key:value" and "key: value" | 
|  | 106 | // are equivalent. | 
|  | 107 | static bool Match(std::string* s, const std::string& prefix) { | 
|  | 108 | if (!android::base::StartsWith(*s, prefix)) return false; | 
|  | 109 | s->assign(android::base::Trim(s->substr(prefix.length()))); | 
|  | 110 | return true; | 
|  | 111 | } | 
|  | 112 |  | 
|  | 113 | static void CollectTests(std::vector<Test>* tests, const char* test_filename) { | 
|  | 114 | std::string absolute_test_filename; | 
|  | 115 | if (!android::base::Realpath(test_filename, &absolute_test_filename)) { | 
|  | 116 | Die(errno, "realpath '%s'", test_filename); | 
|  | 117 | } | 
|  | 118 |  | 
|  | 119 | std::string content; | 
|  | 120 | if (!android::base::ReadFileToString(test_filename, &content)) { | 
|  | 121 | Die(errno, "couldn't read '%s'", test_filename); | 
|  | 122 | } | 
|  | 123 |  | 
|  | 124 | size_t count = 0; | 
|  | 125 | g_file = test_filename; | 
|  | 126 | g_line = 0; | 
|  | 127 | auto lines = android::base::Split(content, "\n"); | 
|  | 128 | std::unique_ptr<Test> test(new Test); | 
|  | 129 | while (g_line < lines.size()) { | 
|  | 130 | auto line = lines[g_line++]; | 
|  | 131 | if (line.empty() || line[0] == '#') continue; | 
|  | 132 |  | 
|  | 133 | if (line[0] == '-') { | 
|  | 134 | if (test->name.empty() || test->command.empty()) { | 
|  | 135 | Die(0, "%s:%zu: each test requires both a name and a command", g_file, g_line); | 
|  | 136 | } | 
|  | 137 | test->test_filename = absolute_test_filename; | 
|  | 138 | tests->push_back(*test.release()); | 
|  | 139 | test.reset(new Test); | 
|  | 140 | ++count; | 
|  | 141 | } else if (Match(&line, "name:")) { | 
|  | 142 | SetField("name", &test->name, line); | 
|  | 143 | } else if (Match(&line, "command:")) { | 
|  | 144 | SetField("command", &test->command, line); | 
|  | 145 | } else if (Match(&line, "before:")) { | 
|  | 146 | test->befores.push_back(line); | 
|  | 147 | } else if (Match(&line, "after:")) { | 
|  | 148 | test->afters.push_back(line); | 
| Elliott Hughes | ac973f6 | 2020-09-25 16:43:35 -0700 | [diff] [blame] | 149 | } else if (Match(&line, "expected-exit-status:")) { | 
|  | 150 | char* end_p; | 
|  | 151 | errno = 0; | 
|  | 152 | test->exit_status = strtol(line.c_str(), &end_p, 10); | 
|  | 153 | if (errno != 0 || *end_p != '\0') { | 
|  | 154 | Die(0, "%s:%zu: bad exit status: \"%s\"", g_file, g_line, line.c_str()); | 
|  | 155 | } | 
| Elliott Hughes | f276140 | 2019-11-15 15:07:00 -0800 | [diff] [blame] | 156 | } else if (Match(&line, "expected-stdout:")) { | 
|  | 157 | // Collect tab-indented lines. | 
|  | 158 | std::string text; | 
|  | 159 | while (g_line < lines.size() && !lines[g_line].empty() && lines[g_line][0] == '\t') { | 
|  | 160 | text += lines[g_line++].substr(1) + "\n"; | 
|  | 161 | } | 
|  | 162 | SetField("expected stdout", &test->expected_stdout, text); | 
|  | 163 | } else { | 
|  | 164 | Die(0, "%s:%zu: syntax error: \"%s\"", g_file, g_line, line.c_str()); | 
|  | 165 | } | 
|  | 166 | } | 
|  | 167 | if (count == 0) Die(0, "no tests found in '%s'", g_file); | 
|  | 168 | } | 
|  | 169 |  | 
|  | 170 | static const char* Plural(size_t n) { | 
|  | 171 | return (n == 1) ? "" : "s"; | 
|  | 172 | } | 
|  | 173 |  | 
|  | 174 | static std::string ExitStatusToString(int status) { | 
|  | 175 | if (WIFSIGNALED(status)) { | 
|  | 176 | return android::base::StringPrintf("was killed by signal %d (%s)", WTERMSIG(status), | 
|  | 177 | strsignal(WTERMSIG(status))); | 
|  | 178 | } | 
|  | 179 | if (WIFSTOPPED(status)) { | 
|  | 180 | return android::base::StringPrintf("was stopped by signal %d (%s)", WSTOPSIG(status), | 
|  | 181 | strsignal(WSTOPSIG(status))); | 
|  | 182 | } | 
|  | 183 | return android::base::StringPrintf("exited with status %d", WEXITSTATUS(status)); | 
|  | 184 | } | 
|  | 185 |  | 
|  | 186 | static bool RunCommands(const char* what, const std::vector<std::string>& commands) { | 
|  | 187 | bool result = true; | 
|  | 188 | for (auto& command : commands) { | 
|  | 189 | V("running %s \"%s\"", what, command.c_str()); | 
|  | 190 | int exit_status = system(command.c_str()); | 
|  | 191 | if (exit_status != 0) { | 
|  | 192 | result = false; | 
|  | 193 | fprintf(stderr, "Command (%s) \"%s\" %s\n", what, command.c_str(), | 
|  | 194 | ExitStatusToString(exit_status).c_str()); | 
|  | 195 | } | 
|  | 196 | } | 
|  | 197 | return result; | 
|  | 198 | } | 
|  | 199 |  | 
|  | 200 | static bool CheckOutput(const char* what, std::string actual_output, | 
|  | 201 | const std::string& expected_output, const std::string& FILES) { | 
|  | 202 | // Rewrite the output to reverse any expansion of $FILES. | 
|  | 203 | actual_output = android::base::StringReplace(actual_output, FILES, "$FILES", true); | 
|  | 204 |  | 
|  | 205 | bool result = (actual_output == expected_output); | 
|  | 206 | if (!result) { | 
|  | 207 | fprintf(stderr, "Incorrect %s.\nExpected:\n%s\nActual:\n%s\n", what, expected_output.c_str(), | 
|  | 208 | actual_output.c_str()); | 
|  | 209 | } | 
|  | 210 | return result; | 
|  | 211 | } | 
|  | 212 |  | 
|  | 213 | static int RunTests(const std::vector<Test>& tests) { | 
|  | 214 | std::vector<std::string> failures; | 
|  | 215 |  | 
|  | 216 | Print(kGreen, "[==========]", " Running %zu tests.", tests.size()); | 
|  | 217 | android::base::Timer total_timer; | 
|  | 218 | for (const auto& test : tests) { | 
|  | 219 | bool failed = false; | 
|  | 220 |  | 
|  | 221 | Print(kGreen, "[ RUN      ]", " %s", test.name.c_str()); | 
|  | 222 | android::base::Timer test_timer; | 
|  | 223 |  | 
|  | 224 | // Set $FILES for this test. | 
|  | 225 | std::string FILES = android::base::Dirname(test.test_filename) + "/files"; | 
|  | 226 | V("setenv(\"FILES\", \"%s\")", FILES.c_str()); | 
|  | 227 | setenv("FILES", FILES.c_str(), 1); | 
|  | 228 |  | 
|  | 229 | // Make a safe space to run the test. | 
|  | 230 | TemporaryDir td; | 
|  | 231 | V("chdir(\"%s\")", td.path); | 
|  | 232 | if (chdir(td.path)) Die(errno, "chdir(\"%s\")", td.path); | 
|  | 233 |  | 
|  | 234 | // Perform any setup specified for this test. | 
|  | 235 | if (!RunCommands("before", test.befores)) failed = true; | 
|  | 236 |  | 
|  | 237 | if (!failed) { | 
|  | 238 | V("running command \"%s\"", test.command.c_str()); | 
|  | 239 | CapturedStdout test_stdout; | 
|  | 240 | CapturedStderr test_stderr; | 
| Elliott Hughes | ac973f6 | 2020-09-25 16:43:35 -0700 | [diff] [blame] | 241 | int status = system(test.command.c_str()); | 
| Elliott Hughes | f276140 | 2019-11-15 15:07:00 -0800 | [diff] [blame] | 242 | test_stdout.Stop(); | 
|  | 243 | test_stderr.Stop(); | 
|  | 244 |  | 
| Elliott Hughes | ac973f6 | 2020-09-25 16:43:35 -0700 | [diff] [blame] | 245 | V("system() returned status %d", status); | 
|  | 246 | if (WEXITSTATUS(status) != test.exit_status) { | 
| Elliott Hughes | f276140 | 2019-11-15 15:07:00 -0800 | [diff] [blame] | 247 | failed = true; | 
|  | 248 | fprintf(stderr, "Incorrect exit status: expected %d but %s\n", test.exit_status, | 
| Elliott Hughes | ac973f6 | 2020-09-25 16:43:35 -0700 | [diff] [blame] | 249 | ExitStatusToString(status).c_str()); | 
| Elliott Hughes | f276140 | 2019-11-15 15:07:00 -0800 | [diff] [blame] | 250 | } | 
|  | 251 |  | 
|  | 252 | if (!CheckOutput("stdout", test_stdout.str(), test.expected_stdout, FILES)) failed = true; | 
|  | 253 | if (!CheckOutput("stderr", test_stderr.str(), test.expected_stderr, FILES)) failed = true; | 
|  | 254 |  | 
|  | 255 | if (!RunCommands("after", test.afters)) failed = true; | 
|  | 256 | } | 
|  | 257 |  | 
|  | 258 | std::stringstream duration; | 
|  | 259 | duration << test_timer; | 
|  | 260 | if (failed) { | 
|  | 261 | failures.push_back(test.name); | 
|  | 262 | Print(kRed, "[  FAILED  ]", " %s (%s)", test.name.c_str(), duration.str().c_str()); | 
|  | 263 | } else { | 
|  | 264 | Print(kGreen, "[       OK ]", " %s (%s)", test.name.c_str(), duration.str().c_str()); | 
|  | 265 | } | 
|  | 266 | } | 
|  | 267 |  | 
|  | 268 | // Summarize the whole run and explicitly list all the failures. | 
|  | 269 |  | 
|  | 270 | std::stringstream duration; | 
|  | 271 | duration << total_timer; | 
|  | 272 | Print(kGreen, "[==========]", " %zu tests ran. (%s total)", tests.size(), duration.str().c_str()); | 
|  | 273 |  | 
|  | 274 | size_t fail_count = failures.size(); | 
|  | 275 | size_t pass_count = tests.size() - fail_count; | 
|  | 276 | Print(kGreen, "[  PASSED  ]", " %zu test%s.", pass_count, Plural(pass_count)); | 
|  | 277 | if (!failures.empty()) { | 
|  | 278 | Print(kRed, "[  FAILED  ]", " %zu test%s.", fail_count, Plural(fail_count)); | 
|  | 279 | for (auto& failure : failures) { | 
|  | 280 | Print(kRed, "[  FAILED  ]", " %s", failure.c_str()); | 
|  | 281 | } | 
|  | 282 | } | 
|  | 283 | return (fail_count == 0) ? 0 : 1; | 
|  | 284 | } | 
|  | 285 |  | 
|  | 286 | static void ShowHelp(bool full) { | 
|  | 287 | fprintf(full ? stdout : stderr, "usage: %s [-v] FILE...\n", g_progname); | 
|  | 288 | if (!full) exit(EXIT_FAILURE); | 
|  | 289 |  | 
|  | 290 | printf( | 
|  | 291 | "\n" | 
|  | 292 | "Run tests.\n" | 
|  | 293 | "\n" | 
|  | 294 | "-v\tVerbose (show workings)\n"); | 
|  | 295 | exit(EXIT_SUCCESS); | 
|  | 296 | } | 
|  | 297 |  | 
|  | 298 | int main(int argc, char* argv[]) { | 
|  | 299 | g_progname = basename(argv[0]); | 
|  | 300 |  | 
|  | 301 | static const struct option opts[] = { | 
|  | 302 | {"help", no_argument, 0, 'h'}, | 
|  | 303 | {"verbose", no_argument, 0, 'v'}, | 
|  | 304 | {}, | 
|  | 305 | }; | 
|  | 306 |  | 
|  | 307 | int opt; | 
|  | 308 | while ((opt = getopt_long(argc, argv, "hv", opts, nullptr)) != -1) { | 
|  | 309 | switch (opt) { | 
|  | 310 | case 'h': | 
|  | 311 | ShowHelp(true); | 
|  | 312 | break; | 
|  | 313 | case 'v': | 
|  | 314 | g_verbose = true; | 
|  | 315 | break; | 
|  | 316 | default: | 
|  | 317 | ShowHelp(false); | 
|  | 318 | break; | 
|  | 319 | } | 
|  | 320 | } | 
|  | 321 |  | 
|  | 322 | argv += optind; | 
|  | 323 | if (!*argv) Die(0, "no test files provided"); | 
|  | 324 | std::vector<Test> tests; | 
|  | 325 | for (; *argv; ++argv) CollectTests(&tests, *argv); | 
|  | 326 | return RunTests(tests); | 
|  | 327 | } |