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