1 // Copyright 2018 The Fuchsia Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include <runtests-utils/runtests-utils.h>
6 
7 #include <ctype.h>
8 #include <dirent.h>
9 #include <errno.h>
10 #include <fcntl.h>
11 #include <glob.h>
12 #include <inttypes.h>
13 #include <libgen.h>
14 #include <limits.h>
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <sys/stat.h>
19 #include <unistd.h>
20 
21 #include <fbl/auto_call.h>
22 #include <fbl/string.h>
23 #include <fbl/string_buffer.h>
24 #include <fbl/string_piece.h>
25 #include <fbl/string_printf.h>
26 #include <fbl/unique_ptr.h>
27 #include <fbl/vector.h>
28 
29 #include <utility>
30 
31 namespace runtests {
32 
ParseTestNames(const fbl::StringPiece input,fbl::Vector<fbl::String> * output)33 void ParseTestNames(const fbl::StringPiece input, fbl::Vector<fbl::String>* output) {
34     // strsep modifies its input, so we have to make a mutable copy.
35     // +1 because StringPiece::size() excludes null terminator.
36     fbl::unique_ptr<char[]> input_copy(new char[input.size() + 1]);
37     memcpy(input_copy.get(), input.data(), input.size());
38     input_copy[input.size()] = '\0';
39 
40     // Tokenize the input string into names.
41     char* next_token;
42     for (char* tmp = strtok_r(input_copy.get(), ",", &next_token); tmp != nullptr;
43          tmp = strtok_r(nullptr, ",", &next_token)) {
44         output->push_back(fbl::String(tmp));
45     }
46 }
47 
IsInWhitelist(const fbl::StringPiece name,const fbl::Vector<fbl::String> & whitelist)48 bool IsInWhitelist(const fbl::StringPiece name, const fbl::Vector<fbl::String>& whitelist) {
49     for (const fbl::String& whitelist_entry : whitelist) {
50         if (name == fbl::StringPiece(whitelist_entry)) {
51             return true;
52         }
53     }
54     return false;
55 }
56 
MkDirAll(const fbl::StringPiece dir_name)57 int MkDirAll(const fbl::StringPiece dir_name) {
58     fbl::StringBuffer<PATH_MAX> dir_buf;
59     if (dir_name.length() > dir_buf.capacity()) {
60         return ENAMETOOLONG;
61     }
62     dir_buf.Append(dir_name);
63     char* dir = dir_buf.data();
64 
65     // Fast path: check if the directory already exists.
66     struct stat s;
67     if (!stat(dir, &s)) {
68         return 0;
69     }
70 
71     // Slow path: create the directory and its parents.
72     for (size_t slash = 0u; dir[slash]; slash++) {
73         if (slash != 0u && dir[slash] == '/') {
74             dir[slash] = '\0';
75             if (mkdir(dir, 0755) && errno != EEXIST) {
76                 return false;
77             }
78             dir[slash] = '/';
79         }
80     }
81     if (mkdir(dir, 0755) && errno != EEXIST) {
82         return errno;
83     }
84     return 0;
85 }
86 
JoinPath(const fbl::StringPiece parent,const fbl::StringPiece child)87 fbl::String JoinPath(const fbl::StringPiece parent, const fbl::StringPiece child) {
88     if (parent.empty()) {
89         return fbl::String(child);
90     }
91     if (child.empty()) {
92         return fbl::String(parent);
93     }
94     if (parent[parent.size() - 1] != '/' && child[0] != '/') {
95         return fbl::String::Concat({parent, "/", child});
96     }
97     if (parent[parent.size() - 1] == '/' && child[0] == '/') {
98         return fbl::String::Concat({parent, &child[1]});
99     }
100     return fbl::String::Concat({parent, child});
101 }
102 
WriteSummaryJSON(const fbl::Vector<fbl::unique_ptr<Result>> & results,const fbl::StringPiece output_file_basename,const fbl::StringPiece syslog_path,FILE * summary_json)103 int WriteSummaryJSON(const fbl::Vector<fbl::unique_ptr<Result>>& results,
104                      const fbl::StringPiece output_file_basename,
105                      const fbl::StringPiece syslog_path,
106                      FILE* summary_json) {
107     int test_count = 0;
108     fprintf(summary_json, "{\n  \"tests\": [\n");
109     for (const fbl::unique_ptr<Result>& result : results) {
110         if (test_count != 0) {
111             fprintf(summary_json, ",\n");
112         }
113         fprintf(summary_json, "    {\n");
114 
115         // Write the name of the test.
116         fprintf(summary_json, "      \"name\": \"%s\",\n", result->name.c_str());
117 
118         // Write the path to the output file, relative to the test output root
119         // (i.e. what's passed in via -o). The test name is already a path to
120         // the test binary on the target, so to make this a relative path, we
121         // only have to skip leading '/' characters in the test name.
122         fbl::String output_file = runtests::JoinPath(result->name, output_file_basename);
123         size_t i = strspn(output_file.c_str(), "/");
124         if (i == output_file.size()) {
125             fprintf(stderr, "Error: output_file was empty or all slashes: %s\n",
126                     output_file.c_str());
127             return EINVAL;
128         }
129         fprintf(summary_json, "      \"output_file\": \"%s\",\n", &(output_file.c_str()[i]));
130 
131         // Write the result of the test, which is either PASS or FAIL. We only
132         // have one PASS condition in TestResult, which is SUCCESS.
133         fprintf(summary_json, "      \"result\": \"%s\"",
134                 result->launch_status == runtests::SUCCESS ? "PASS" : "FAIL");
135 
136         // Write all data sinks.
137         if (result->data_sinks.size()) {
138             fprintf(summary_json, ",\n      \"data_sinks\": {\n");
139             int sink_count = 0;
140             for (const auto& sink : result->data_sinks) {
141                 if (sink_count != 0) {
142                     fprintf(summary_json, ",\n");
143                 }
144                 fprintf(summary_json, "        \"%s\": [\n", sink.name.c_str());
145                 int file_count = 0;
146                 for (const auto& file : sink.files) {
147                     if (file_count != 0) {
148                         fprintf(summary_json, ",\n");
149                     }
150                     fprintf(summary_json, "          {\n"
151                                           "            \"name\": \"%s\",\n"
152                                           "            \"file\": \"%s\"\n"
153                                           "          }",
154                             file.name.c_str(), file.file.c_str());
155                     file_count++;
156                 }
157                 fprintf(summary_json, "\n        ]");
158                 sink_count++;
159             }
160             fprintf(summary_json, "\n      }\n");
161         } else {
162             fprintf(summary_json, "\n");
163         }
164         fprintf(summary_json, "    }");
165         test_count++;
166     }
167     fprintf(summary_json, "\n  ]");
168     if (!syslog_path.empty()) {
169         fprintf(summary_json, ",\n"
170                               "  \"outputs\": {\n"
171                               "    \"syslog_file\": \"%.*s\"\n"
172                               "  }\n",
173                 static_cast<int>(syslog_path.length()), syslog_path.data());
174     } else {
175         fprintf(summary_json, "\n");
176     }
177     fprintf(summary_json, "}\n");
178     return 0;
179 }
180 
ResolveGlobs(const fbl::Vector<fbl::String> & globs,fbl::Vector<fbl::String> * resolved)181 int ResolveGlobs(const fbl::Vector<fbl::String>& globs,
182                  fbl::Vector<fbl::String>* resolved) {
183     glob_t resolved_glob;
184     auto auto_call_glob_free = fbl::MakeAutoCall([&resolved_glob] { globfree(&resolved_glob); });
185     int flags = 0;
186     for (const auto& test_dir_glob : globs) {
187         int err = glob(test_dir_glob.c_str(), flags, nullptr, &resolved_glob);
188 
189         // Ignore a lack of matches.
190         if (err && err != GLOB_NOMATCH) {
191             return err;
192         }
193         flags = GLOB_APPEND;
194     }
195     resolved->reserve(resolved_glob.gl_pathc);
196     for (size_t i = 0; i < resolved_glob.gl_pathc; ++i) {
197         resolved->push_back(fbl::String(resolved_glob.gl_pathv[i]));
198     }
199     return 0;
200 }
201 
DiscoverTestsInDirGlobs(const fbl::Vector<fbl::String> & dir_globs,const char * ignore_dir_name,const fbl::Vector<fbl::String> & basename_whitelist,fbl::Vector<fbl::String> * test_paths)202 int DiscoverTestsInDirGlobs(const fbl::Vector<fbl::String>& dir_globs,
203                             const char* ignore_dir_name,
204                             const fbl::Vector<fbl::String>& basename_whitelist,
205                             fbl::Vector<fbl::String>* test_paths) {
206     fbl::Vector<fbl::String> test_dirs;
207     const int err = ResolveGlobs(dir_globs, &test_dirs);
208     if (err) {
209         fprintf(stderr, "Error: Failed to resolve globs, error = %d\n", err);
210         return EIO; // glob()'s return values aren't the same as errno. This is somewhat arbitrary.
211     }
212     for (const fbl::String& test_dir : test_dirs) {
213         // In the event of failures around a directory not existing or being an empty node
214         // we will continue to the next entries rather than aborting. This allows us to handle
215         // different sets of default test directories.
216         struct stat st;
217         if (stat(test_dir.c_str(), &st) < 0) {
218             printf("Could not stat %s, skipping...\n", test_dir.c_str());
219             continue;
220         }
221         if (!S_ISDIR(st.st_mode)) {
222             // Silently skip non-directories, as they may have been picked up in
223             // the glob.
224             continue;
225         }
226 
227         // Resolve an absolute path to the test directory to ensure output
228         // directory names will never collide.
229         char abs_test_dir[PATH_MAX];
230         if (realpath(test_dir.c_str(), abs_test_dir) == nullptr) {
231             printf("Error: Could not resolve path %s: %s\n", test_dir.c_str(), strerror(errno));
232             continue;
233         }
234 
235         // Silently skip |ignore_dir_name|.
236         // The user may have done something like runtests /foo/bar/h*.
237         const auto test_dir_base = basename(abs_test_dir);
238         if (ignore_dir_name && strcmp(test_dir_base, ignore_dir_name) == 0) {
239             continue;
240         }
241 
242         DIR* dir = opendir(abs_test_dir);
243         if (dir == nullptr) {
244             fprintf(stderr, "Error: Could not open test dir %s\n", abs_test_dir);
245             return errno;
246         }
247 
248         struct dirent* de;
249         while ((de = readdir(dir)) != nullptr) {
250             const char* test_name = de->d_name;
251             if (!basename_whitelist.is_empty() &&
252                 !runtests::IsInWhitelist(test_name, basename_whitelist)) {
253                 continue;
254             }
255 
256             const fbl::String test_path = runtests::JoinPath(abs_test_dir, test_name);
257             if (stat(test_path.c_str(), &st) != 0 || !S_ISREG(st.st_mode)) {
258                 continue;
259             }
260             test_paths->push_back(test_path);
261         }
262         closedir(dir);
263     }
264     return 0;
265 }
266 
DiscoverTestsInListFile(FILE * test_list_file,fbl::Vector<fbl::String> * test_paths)267 int DiscoverTestsInListFile(FILE* test_list_file, fbl::Vector<fbl::String>* test_paths) {
268     char* line = nullptr;
269     size_t line_capacity = 0;
270     auto free_line = fbl::MakeAutoCall([&line]() {
271         free(line);
272     });
273     while (true) {
274         ssize_t line_length = getline(&line, &line_capacity, test_list_file);
275         if (line_length < 0) {
276             if (feof(test_list_file)) {
277                 break;
278             }
279             return errno;
280         }
281         // Don't include trailing space.
282         while (line_length && isspace(line[line_length - 1])) {
283             line_length -= 1;
284         }
285         if (!line_length) {
286             continue;
287         }
288         line[line_length] = '\0';
289         test_paths->push_back(line);
290     }
291     return 0;
292 }
293 
RunTests(const RunTestFn & RunTest,const fbl::Vector<fbl::String> & test_paths,const fbl::Vector<fbl::String> & test_args,const char * output_dir,const fbl::StringPiece output_file_basename,signed char verbosity,int * failed_count,fbl::Vector<fbl::unique_ptr<Result>> * results)294 bool RunTests(const RunTestFn& RunTest, const fbl::Vector<fbl::String>& test_paths,
295               const fbl::Vector<fbl::String>& test_args,
296               const char* output_dir,
297               const fbl::StringPiece output_file_basename, signed char verbosity, int* failed_count,
298               fbl::Vector<fbl::unique_ptr<Result>>* results) {
299     for (const fbl::String& test_path : test_paths) {
300         fbl::String output_dir_for_test_str;
301         fbl::String output_filename_str;
302         // Ensure the output directory for this test binary's output exists.
303         if (output_dir != nullptr) {
304             // If output_dir was specified, ask |RunTest| to redirect stdout/stderr
305             // to a file whose name is based on the test name.
306             output_dir_for_test_str = runtests::JoinPath(output_dir, test_path);
307             const int error = runtests::MkDirAll(output_dir_for_test_str);
308             if (error) {
309                 fprintf(stderr, "Error: Could not create output directory %s: %s\n",
310                         output_dir_for_test_str.c_str(), strerror(error));
311                 return false;
312             }
313             output_filename_str = JoinPath(output_dir_for_test_str, output_file_basename);
314         }
315 
316         // Assemble test binary args.
317         fbl::Vector<const char*> argv;
318         argv.push_back(test_path.c_str());
319         fbl::String verbosity_arg;
320         if (verbosity >= 0) {
321             // verbosity defaults to -1: "unspecified". Only pass it along
322             // if it was specified: i.e., non-negative.
323             verbosity_arg = fbl::StringPrintf("v=%d", verbosity);
324             argv.push_back(verbosity_arg.c_str());
325         }
326         // Add in args to the test binary
327         argv.reserve(test_args.size());
328         for (auto test_arg = test_args.begin(); test_arg != test_args.end(); ++test_arg) {
329             argv.push_back(test_arg->c_str());
330         }
331         argv.push_back(nullptr); // Important, since there's no argc.
332         const char* output_dir_for_test =
333             output_dir_for_test_str.empty() ? nullptr : output_dir_for_test_str.c_str();
334         const char* output_filename =
335             output_filename_str.empty() ? nullptr : output_filename_str.c_str();
336 
337         // Execute the test binary.
338         printf("\n------------------------------------------------\n"
339                "RUNNING TEST: %s\n\n",
340                test_path.c_str());
341         fbl::unique_ptr<Result> result = RunTest(argv.get(), output_dir_for_test,
342                                                  output_filename);
343         if (result->launch_status != SUCCESS) {
344             *failed_count += 1;
345         }
346         results->push_back(std::move(result));
347     }
348     return true;
349 }
350 
351 } // namespace runtests
352