1 // Copyright 2017-2018 ccls Authors
2 // SPDX-License-Identifier: Apache-2.0
3 
4 #include "test.hh"
5 
6 #include "filesystem.hh"
7 #include "indexer.hh"
8 #include "pipeline.hh"
9 #include "platform.hh"
10 #include "sema_manager.hh"
11 #include "serializer.hh"
12 #include "utils.hh"
13 
14 #include <llvm/ADT/StringRef.h>
15 #include <llvm/Config/llvm-config.h>
16 
17 #include <rapidjson/document.h>
18 #include <rapidjson/prettywriter.h>
19 #include <rapidjson/stringbuffer.h>
20 #include <rapidjson/writer.h>
21 
22 #include <fstream>
23 #include <stdio.h>
24 #include <stdlib.h>
25 
26 // The 'diff' utility is available and we can use dprintf(3).
27 #if _POSIX_C_SOURCE >= 200809L
28 #include <sys/wait.h>
29 #include <unistd.h>
30 #endif
31 
32 using namespace llvm;
33 
34 extern bool gTestOutputMode;
35 
36 namespace ccls {
toString(const rapidjson::Document & document)37 std::string toString(const rapidjson::Document &document) {
38   rapidjson::StringBuffer buffer;
39   rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
40   writer.SetFormatOptions(
41       rapidjson::PrettyFormatOptions::kFormatSingleLineArray);
42   writer.SetIndent(' ', 2);
43 
44   buffer.Clear();
45   document.Accept(writer);
46   return buffer.GetString();
47 }
48 
49 struct TextReplacer {
50   struct Replacement {
51     std::string from;
52     std::string to;
53   };
54 
55   std::vector<Replacement> replacements;
56 
applyccls::TextReplacer57   std::string apply(const std::string &content) {
58     std::string result = content;
59 
60     for (const Replacement &replacement : replacements) {
61       while (true) {
62         size_t idx = result.find(replacement.from);
63         if (idx == std::string::npos)
64           break;
65 
66         result.replace(result.begin() + idx,
67                        result.begin() + idx + replacement.from.size(),
68                        replacement.to);
69       }
70     }
71 
72     return result;
73   }
74 };
75 
trimInPlace(std::string & s)76 void trimInPlace(std::string &s) {
77   auto f = [](char c) { return !isspace(c); };
78   s.erase(s.begin(), std::find_if(s.begin(), s.end(), f));
79   s.erase(std::find_if(s.rbegin(), s.rend(), f).base(), s.end());
80 }
81 
splitString(const std::string & str,const std::string & delimiter)82 std::vector<std::string> splitString(const std::string &str,
83                                      const std::string &delimiter) {
84   // http://stackoverflow.com/a/13172514
85   std::vector<std::string> strings;
86 
87   std::string::size_type pos = 0;
88   std::string::size_type prev = 0;
89   while ((pos = str.find(delimiter, prev)) != std::string::npos) {
90     strings.push_back(str.substr(prev, pos - prev));
91     prev = pos + 1;
92   }
93 
94   // To get the last substring (or only, if delimiter is not found)
95   strings.push_back(str.substr(prev));
96 
97   return strings;
98 }
99 
parseTestExpectation(const std::string & filename,const std::vector<std::string> & lines_with_endings,TextReplacer * replacer,std::vector<std::string> * flags,std::unordered_map<std::string,std::string> * output_sections)100 void parseTestExpectation(
101     const std::string &filename,
102     const std::vector<std::string> &lines_with_endings, TextReplacer *replacer,
103     std::vector<std::string> *flags,
104     std::unordered_map<std::string, std::string> *output_sections) {
105   // Scan for EXTRA_FLAGS:
106   {
107     bool in_output = false;
108     for (StringRef line : lines_with_endings) {
109       line = line.trim();
110 
111       if (line.startswith("EXTRA_FLAGS:")) {
112         assert(!in_output && "multiple EXTRA_FLAGS sections");
113         in_output = true;
114         continue;
115       }
116 
117       if (in_output && line.empty())
118         break;
119 
120       if (in_output)
121         flags->push_back(line.str());
122     }
123   }
124 
125   // Scan for OUTPUT:
126   {
127     std::string active_output_filename;
128     std::string active_output_contents;
129 
130     bool in_output = false;
131     for (StringRef line_with_ending : lines_with_endings) {
132       if (line_with_ending.startswith("*/"))
133         break;
134 
135       if (line_with_ending.startswith("OUTPUT:")) {
136         // Terminate the previous output section if we found a new one.
137         if (in_output) {
138           (*output_sections)[active_output_filename] = active_output_contents;
139         }
140 
141         // Try to tokenize OUTPUT: based one whitespace. If there is more than
142         // one token assume it is a filename.
143         SmallVector<StringRef, 2> tokens;
144         line_with_ending.split(tokens, ' ');
145         if (tokens.size() > 1) {
146           active_output_filename = tokens[1].str();
147         } else {
148           active_output_filename = filename;
149         }
150         active_output_contents = "";
151 
152         in_output = true;
153       } else if (in_output) {
154         active_output_contents += line_with_ending;
155         active_output_contents.push_back('\n');
156       }
157     }
158 
159     if (in_output)
160       (*output_sections)[active_output_filename] = active_output_contents;
161   }
162 }
163 
updateTestExpectation(const std::string & filename,const std::string & expectation,const std::string & actual)164 void updateTestExpectation(const std::string &filename,
165                            const std::string &expectation,
166                            const std::string &actual) {
167   // Read the entire file into a string.
168   std::ifstream in(filename);
169   std::string str;
170   str.assign(std::istreambuf_iterator<char>(in),
171              std::istreambuf_iterator<char>());
172   in.close();
173 
174   // Replace expectation
175   auto it = str.find(expectation);
176   assert(it != std::string::npos);
177   str.replace(it, expectation.size(), actual);
178 
179   // Write it back out.
180   writeToFile(filename, str);
181 }
182 
diffDocuments(std::string path,std::string path_section,rapidjson::Document & expected,rapidjson::Document & actual)183 void diffDocuments(std::string path, std::string path_section,
184                    rapidjson::Document &expected, rapidjson::Document &actual) {
185   std::string joined_actual_output = toString(actual);
186   std::string joined_expected_output = toString(expected);
187   printf("[FAILED] %s (section %s)\n", path.c_str(), path_section.c_str());
188 
189 #if _POSIX_C_SOURCE >= 200809L
190   char expected_file[] = "/tmp/ccls.expected.XXXXXX";
191   char actual_file[] = "/tmp/ccls.actual.XXXXXX";
192   int expected_fd = mkstemp(expected_file);
193   int actual_fd = mkstemp(actual_file);
194   dprintf(expected_fd, "%s", joined_expected_output.c_str());
195   dprintf(actual_fd, "%s", joined_actual_output.c_str());
196   close(expected_fd);
197   close(actual_fd);
198   pid_t child = fork();
199   if (child == 0) {
200     execlp("diff", "diff", "-U", "3", expected_file, actual_file, NULL);
201     _Exit(127);
202   } else {
203     int status;
204     waitpid(child, &status, 0);
205     unlink(expected_file);
206     unlink(actual_file);
207     // 'diff' returns 0 or 1 if exitted normaly.
208     if (WEXITSTATUS(status) <= 1)
209       return;
210   }
211 #endif
212   std::vector<std::string> actual_output =
213       splitString(joined_actual_output, "\n");
214   std::vector<std::string> expected_output =
215       splitString(joined_expected_output, "\n");
216 
217   printf("Expected output for %s (section %s)\n:%s\n", path.c_str(),
218          path_section.c_str(), joined_expected_output.c_str());
219   printf("Actual output for %s (section %s)\n:%s\n", path.c_str(),
220          path_section.c_str(), joined_actual_output.c_str());
221 }
222 
verifySerializeToFrom(IndexFile * file)223 void verifySerializeToFrom(IndexFile *file) {
224   std::string expected = file->toString();
225   std::string serialized = ccls::serialize(SerializeFormat::Json, *file);
226   std::unique_ptr<IndexFile> result =
227       ccls::deserialize(SerializeFormat::Json, "--.cc", serialized, "<empty>",
228                         std::nullopt /*expected_version*/);
229   std::string actual = result->toString();
230   if (expected != actual) {
231     fprintf(stderr, "Serialization failure\n");
232     // assert(false);
233   }
234 }
235 
findExpectedOutputForFilename(std::string filename,const std::unordered_map<std::string,std::string> & expected)236 std::string findExpectedOutputForFilename(
237     std::string filename,
238     const std::unordered_map<std::string, std::string> &expected) {
239   for (const auto &entry : expected) {
240     if (StringRef(entry.first).endswith(filename))
241       return entry.second;
242   }
243 
244   fprintf(stderr, "Couldn't find expected output for %s\n", filename.c_str());
245   getchar();
246   getchar();
247   return "{}";
248 }
249 
250 IndexFile *
findDbForPathEnding(const std::string & path,const std::vector<std::unique_ptr<IndexFile>> & dbs)251 findDbForPathEnding(const std::string &path,
252                     const std::vector<std::unique_ptr<IndexFile>> &dbs) {
253   for (auto &db : dbs) {
254     if (StringRef(db->path).endswith(path))
255       return db.get();
256   }
257   return nullptr;
258 }
259 
runIndexTests(const std::string & filter_path,bool enable_update)260 bool runIndexTests(const std::string &filter_path, bool enable_update) {
261   gTestOutputMode = true;
262   std::string version = LLVM_VERSION_STRING;
263 
264   // Index tests change based on the version of clang used.
265   static const char kRequiredClangVersion[] = "6.0.0";
266   if (version != kRequiredClangVersion &&
267       version.find("svn") == std::string::npos) {
268     fprintf(stderr,
269             "Index tests must be run using clang version %s, ccls is running "
270             "with %s\n",
271             kRequiredClangVersion, version.c_str());
272     return false;
273   }
274 
275   bool success = true;
276   bool update_all = false;
277   // FIXME: show diagnostics in STL/headers when running tests. At the moment
278   // this can be done by conRequestIdex index(1, 1);
279   SemaManager completion(
280       nullptr, nullptr, [&](std::string, std::vector<Diagnostic>) {},
281       [](RequestId id) {});
282   getFilesInFolder(
283       "index_tests", true /*recursive*/, true /*add_folder_to_path*/,
284       [&](const std::string &path) {
285         bool is_fail_allowed = false;
286 
287         if (path.find(filter_path) == std::string::npos)
288           return;
289 
290         if (!filter_path.empty())
291           printf("Running %s\n", path.c_str());
292 
293         // Parse expected output from the test, parse it into JSON document.
294         std::vector<std::string> lines_with_endings;
295         {
296           std::ifstream fin(path);
297           for (std::string line; std::getline(fin, line);)
298             lines_with_endings.push_back(line);
299         }
300         TextReplacer text_replacer;
301         std::vector<std::string> flags;
302         std::unordered_map<std::string, std::string> all_expected_output;
303         parseTestExpectation(path, lines_with_endings, &text_replacer, &flags,
304                              &all_expected_output);
305 
306         // Build flags.
307         flags.push_back("-resource-dir=" + getDefaultResourceDirectory());
308         flags.push_back(path);
309 
310         // Run test.
311         g_config = new Config;
312         VFS vfs;
313         WorkingFiles wfiles;
314         std::vector<const char *> cargs;
315         for (auto &arg : flags)
316           cargs.push_back(arg.c_str());
317         bool ok;
318         auto result = ccls::idx::index(&completion, &wfiles, &vfs, "", path,
319                                        cargs, {}, true, ok);
320 
321         for (const auto &entry : all_expected_output) {
322           const std::string &expected_path = entry.first;
323           std::string expected_output = text_replacer.apply(entry.second);
324 
325           // Get output from index operation.
326           IndexFile *db = findDbForPathEnding(expected_path, result.indexes);
327           std::string actual_output = "{}";
328           if (db) {
329             verifySerializeToFrom(db);
330             actual_output = db->toString();
331           }
332           actual_output = text_replacer.apply(actual_output);
333 
334           // Compare output via rapidjson::Document to ignore any formatting
335           // differences.
336           rapidjson::Document actual;
337           actual.Parse(actual_output.c_str());
338           rapidjson::Document expected;
339           expected.Parse(expected_output.c_str());
340 
341           if (actual == expected) {
342             // std::cout << "[PASSED] " << path << std::endl;
343           } else {
344             if (!is_fail_allowed)
345               success = false;
346             diffDocuments(path, expected_path, expected, actual);
347             puts("\n");
348             if (enable_update) {
349               printf("[Enter to continue - type u to update test, a to update "
350                      "all]");
351               char c = 'u';
352               if (!update_all) {
353                 c = getchar();
354                 getchar();
355               }
356 
357               if (c == 'a')
358                 update_all = true;
359 
360               if (update_all || c == 'u') {
361                 // Note: we use |entry.second| instead of |expected_output|
362                 // because
363                 // |expected_output| has had text replacements applied.
364                 updateTestExpectation(path, entry.second,
365                                       toString(actual) + "\n");
366               }
367             }
368           }
369         }
370       });
371 
372   return success;
373 }
374 } // namespace ccls
375