ccls/src/test.cc

366 lines
11 KiB
C++
Raw Normal View History

2018-08-21 05:27:52 +00:00
// Copyright 2017-2018 ccls Authors
// SPDX-License-Identifier: Apache-2.0
2018-10-29 04:21:21 +00:00
#include "test.hh"
2017-03-10 07:06:01 +00:00
#include "clang_complete.hh"
2018-04-08 00:10:54 +00:00
#include "filesystem.hh"
2018-10-29 04:21:21 +00:00
#include "indexer.hh"
2018-09-21 01:04:55 +00:00
#include "pipeline.hh"
2018-10-29 04:21:21 +00:00
#include "platform.hh"
#include "serializer.hh"
2018-10-29 04:21:21 +00:00
#include "utils.hh"
2017-02-27 07:28:33 +00:00
2018-07-15 07:45:51 +00:00
#include <llvm/Config/llvm-config.h>
2018-09-21 00:20:09 +00:00
#include <llvm/ADT/StringRef.h>
using namespace llvm;
2018-07-15 07:45:51 +00:00
2018-02-24 02:30:06 +00:00
#include <rapidjson/document.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
2018-02-24 02:24:54 +00:00
2018-08-09 17:08:14 +00:00
#include <fstream>
#include <stdio.h>
2018-01-28 06:31:17 +00:00
#include <stdlib.h>
2018-01-10 08:15:58 +00:00
2018-01-28 06:31:17 +00:00
// The 'diff' utility is available and we can use dprintf(3).
#if _POSIX_C_SOURCE >= 200809L
#include <sys/wait.h>
#include <unistd.h>
#endif
2018-04-08 00:10:54 +00:00
extern bool gTestOutputMode;
namespace ccls {
2018-08-09 17:08:14 +00:00
std::string ToString(const rapidjson::Document &document) {
2017-02-27 07:28:33 +00:00
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
writer.SetFormatOptions(
2017-09-22 01:14:57 +00:00
rapidjson::PrettyFormatOptions::kFormatSingleLineArray);
2017-02-27 07:28:33 +00:00
writer.SetIndent(' ', 2);
buffer.Clear();
document.Accept(writer);
2018-03-31 16:25:58 +00:00
return buffer.GetString();
2017-02-27 07:28:33 +00:00
}
2018-04-08 00:10:54 +00:00
struct TextReplacer {
struct Replacement {
std::string from;
std::string to;
};
std::vector<Replacement> replacements;
2018-08-09 17:08:14 +00:00
std::string Apply(const std::string &content) {
2018-04-08 00:10:54 +00:00
std::string result = content;
2018-08-09 17:08:14 +00:00
for (const Replacement &replacement : replacements) {
2018-04-08 00:10:54 +00:00
while (true) {
size_t idx = result.find(replacement.from);
if (idx == std::string::npos)
break;
result.replace(result.begin() + idx,
2018-08-09 17:08:14 +00:00
result.begin() + idx + replacement.from.size(),
replacement.to);
2018-04-08 00:10:54 +00:00
}
}
return result;
}
};
2018-09-21 00:20:09 +00:00
void TrimInPlace(std::string &s) {
auto f = [](char c) { return !isspace(c); };
s.erase(s.begin(), std::find_if(s.begin(), s.end(), f));
s.erase(std::find_if(s.rbegin(), s.rend(), f).base(), s.end());
}
2018-02-24 02:24:54 +00:00
void ParseTestExpectation(
2018-08-09 17:08:14 +00:00
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) {
2018-02-24 02:24:54 +00:00
// Scan for EXTRA_FLAGS:
{
bool in_output = false;
for (std::string line : lines_with_endings) {
2018-09-21 00:20:09 +00:00
line = StringRef(line).trim().str();
2018-02-24 02:24:54 +00:00
if (StartsWith(line, "EXTRA_FLAGS:")) {
assert(!in_output && "multiple EXTRA_FLAGS sections");
in_output = true;
continue;
}
if (in_output && line.empty())
break;
if (in_output)
flags->push_back(line);
}
}
// Scan for OUTPUT:
{
std::string active_output_filename;
std::string active_output_contents;
bool in_output = false;
for (std::string line_with_ending : lines_with_endings) {
if (StartsWith(line_with_ending, "*/"))
break;
if (StartsWith(line_with_ending, "OUTPUT:")) {
// Terminate the previous output section if we found a new one.
if (in_output) {
(*output_sections)[active_output_filename] = active_output_contents;
}
// Try to tokenize OUTPUT: based one whitespace. If there is more than
// one token assume it is a filename.
std::vector<std::string> tokens = SplitString(line_with_ending, " ");
if (tokens.size() > 1) {
2018-09-21 00:20:09 +00:00
active_output_filename = StringRef(tokens[1]).trim().str();
2018-02-24 02:24:54 +00:00
} else {
active_output_filename = filename;
}
active_output_contents = "";
in_output = true;
} else if (in_output) {
2018-02-24 02:24:54 +00:00
active_output_contents += line_with_ending;
active_output_contents.push_back('\n');
}
2018-02-24 02:24:54 +00:00
}
if (in_output)
(*output_sections)[active_output_filename] = active_output_contents;
}
}
2018-08-09 17:08:14 +00:00
void UpdateTestExpectation(const std::string &filename,
const std::string &expectation,
const std::string &actual) {
2018-02-24 02:24:54 +00:00
// Read the entire file into a string.
std::ifstream in(filename);
std::string str;
str.assign(std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>());
in.close();
// Replace expectation
auto it = str.find(expectation);
assert(it != std::string::npos);
str.replace(it, expectation.size(), actual);
// Write it back out.
WriteToFile(filename, str);
}
2018-08-09 17:08:14 +00:00
void DiffDocuments(std::string path, std::string path_section,
rapidjson::Document &expected, rapidjson::Document &actual) {
2017-04-03 01:34:15 +00:00
std::string joined_actual_output = ToString(actual);
2018-01-28 06:31:17 +00:00
std::string joined_expected_output = ToString(expected);
2018-04-08 00:10:54 +00:00
printf("[FAILED] %s (section %s)\n", path.c_str(), path_section.c_str());
2018-01-28 06:31:17 +00:00
#if _POSIX_C_SOURCE >= 200809L
2018-03-31 03:16:33 +00:00
char expected_file[] = "/tmp/ccls.expected.XXXXXX";
char actual_file[] = "/tmp/ccls.actual.XXXXXX";
2018-01-28 06:31:17 +00:00
int expected_fd = mkstemp(expected_file);
int actual_fd = mkstemp(actual_file);
dprintf(expected_fd, "%s", joined_expected_output.c_str());
dprintf(actual_fd, "%s", joined_actual_output.c_str());
close(expected_fd);
close(actual_fd);
pid_t child = fork();
if (child == 0) {
execlp("diff", "diff", "-U", "3", expected_file, actual_file, NULL);
_Exit(127);
} else {
int status;
waitpid(child, &status, 0);
unlink(expected_file);
unlink(actual_file);
// 'diff' returns 0 or 1 if exitted normaly.
if (WEXITSTATUS(status) <= 1)
return;
}
#endif
2017-09-22 01:14:57 +00:00
std::vector<std::string> actual_output =
SplitString(joined_actual_output, "\n");
std::vector<std::string> expected_output =
SplitString(joined_expected_output, "\n");
2017-02-27 07:28:33 +00:00
2018-04-08 00:10:54 +00:00
printf("Expected output for %s (section %s)\n:%s\n", path.c_str(),
path_section.c_str(), joined_expected_output.c_str());
printf("Actual output for %s (section %s)\n:%s\n", path.c_str(),
path_section.c_str(), joined_actual_output.c_str());
2017-02-27 07:28:33 +00:00
}
2018-08-09 17:08:14 +00:00
void VerifySerializeToFrom(IndexFile *file) {
std::string expected = file->ToString();
std::string serialized = ccls::Serialize(SerializeFormat::Json, *file);
std::unique_ptr<IndexFile> result =
ccls::Deserialize(SerializeFormat::Json, "--.cc", serialized, "<empty>",
std::nullopt /*expected_version*/);
std::string actual = result->ToString();
2017-03-01 04:12:57 +00:00
if (expected != actual) {
2018-04-08 00:10:54 +00:00
fprintf(stderr, "Serialization failure\n");
2018-08-09 17:08:14 +00:00
// assert(false);
2017-03-01 04:12:57 +00:00
}
}
2017-09-22 01:14:57 +00:00
std::string FindExpectedOutputForFilename(
std::string filename,
2018-08-09 17:08:14 +00:00
const std::unordered_map<std::string, std::string> &expected) {
for (const auto &entry : expected) {
2017-04-08 22:54:36 +00:00
if (EndsWith(entry.first, filename))
return entry.second;
}
2017-02-27 07:28:33 +00:00
2018-04-08 00:10:54 +00:00
fprintf(stderr, "Couldn't find expected output for %s\n", filename.c_str());
getchar();
getchar();
2017-04-08 22:54:36 +00:00
return "{}";
}
2018-08-09 17:08:14 +00:00
IndexFile *
FindDbForPathEnding(const std::string &path,
const std::vector<std::unique_ptr<IndexFile>> &dbs) {
for (auto &db : dbs) {
2017-04-08 22:54:36 +00:00
if (EndsWith(db->path, path))
return db.get();
}
return nullptr;
}
2018-08-09 17:08:14 +00:00
bool RunIndexTests(const std::string &filter_path, bool enable_update) {
2018-04-08 00:10:54 +00:00
gTestOutputMode = true;
2018-07-15 07:45:51 +00:00
std::string version = LLVM_VERSION_STRING;
// Index tests change based on the version of clang used.
2018-07-15 07:45:51 +00:00
static const char kRequiredClangVersion[] = "6.0.0";
if (version != kRequiredClangVersion &&
2018-07-15 07:45:51 +00:00
version.find("svn") == std::string::npos) {
2018-04-08 00:10:54 +00:00
fprintf(stderr,
"Index tests must be run using clang version %s, ccls is running "
"with %s\n",
kRequiredClangVersion, version.c_str());
2018-01-18 05:29:32 +00:00
return false;
}
bool success = true;
bool update_all = false;
2018-01-07 10:39:54 +00:00
// FIXME: show diagnostics in STL/headers when running tests. At the moment
// this can be done by constructing ClangIndex index(1, 1);
2018-09-19 16:31:45 +00:00
CompletionManager completion(
nullptr, nullptr, [&](std::string, std::vector<lsDiagnostic>) {},
[](lsRequestId id) {});
2018-04-08 00:10:54 +00:00
GetFilesInFolder(
"index_tests", true /*recursive*/, true /*add_folder_to_path*/,
2018-08-09 17:08:14 +00:00
[&](const std::string &path) {
2018-04-08 00:10:54 +00:00
bool is_fail_allowed = false;
2018-04-08 00:10:54 +00:00
if (EndsWithAny(path, {".m", ".mm"})) {
2018-04-04 07:43:37 +00:00
#ifndef __APPLE__
2018-04-08 00:10:54 +00:00
return;
2018-04-04 07:43:37 +00:00
#endif
2018-04-08 00:10:54 +00:00
// objective-c tests are often not updated right away. do not bring
// down
// CI if they fail.
if (!enable_update)
is_fail_allowed = true;
}
2018-04-08 00:10:54 +00:00
if (path.find(filter_path) == std::string::npos)
return;
if (!filter_path.empty())
printf("Running %s\n", path.c_str());
2018-04-08 00:10:54 +00:00
// Parse expected output from the test, parse it into JSON document.
std::vector<std::string> lines_with_endings;
{
std::ifstream fin(path);
for (std::string line; std::getline(fin, line);)
lines_with_endings.push_back(line);
}
TextReplacer text_replacer;
std::vector<std::string> flags;
std::unordered_map<std::string, std::string> all_expected_output;
ParseTestExpectation(path, lines_with_endings, &text_replacer, &flags,
&all_expected_output);
// Build flags.
flags.push_back("-resource-dir=" + GetDefaultResourceDirectory());
flags.push_back(path);
// Run test.
g_config = new Config;
VFS vfs;
WorkingFiles wfiles;
2018-09-19 16:31:45 +00:00
std::vector<const char *> cargs;
for (auto &arg : flags)
cargs.push_back(arg.c_str());
2018-09-23 01:00:50 +00:00
bool ok;
auto dbs = ccls::idx::Index(&completion, &wfiles, &vfs, "", path, cargs, {}, ok);
2018-04-08 00:10:54 +00:00
2018-08-09 17:08:14 +00:00
for (const auto &entry : all_expected_output) {
const std::string &expected_path = entry.first;
2018-04-08 00:10:54 +00:00
std::string expected_output = text_replacer.Apply(entry.second);
// Get output from index operation.
2018-08-09 17:08:14 +00:00
IndexFile *db = FindDbForPathEnding(expected_path, dbs);
2018-04-08 00:10:54 +00:00
std::string actual_output = "{}";
if (db) {
VerifySerializeToFrom(db);
actual_output = db->ToString();
}
actual_output = text_replacer.Apply(actual_output);
// Compare output via rapidjson::Document to ignore any formatting
// differences.
rapidjson::Document actual;
actual.Parse(actual_output.c_str());
rapidjson::Document expected;
expected.Parse(expected_output.c_str());
if (actual == expected) {
// std::cout << "[PASSED] " << path << std::endl;
} else {
if (!is_fail_allowed)
success = false;
DiffDocuments(path, expected_path, expected, actual);
puts("\n");
if (enable_update) {
2018-08-09 17:08:14 +00:00
printf("[Enter to continue - type u to update test, a to update "
"all]");
2018-04-08 00:10:54 +00:00
char c = 'u';
if (!update_all) {
c = getchar();
getchar();
}
if (c == 'a')
update_all = true;
if (update_all || c == 'u') {
// Note: we use |entry.second| instead of |expected_output|
// because
// |expected_output| has had text replacements applied.
UpdateTestExpectation(path, entry.second,
ToString(actual) + "\n");
}
}
}
}
2018-04-08 00:10:54 +00:00
});
return success;
2017-02-27 07:28:33 +00:00
}
} // namespace ccls