mirror of
https://github.com/MaskRay/ccls.git
synced 2024-11-25 17:11:59 +00:00
Make some good progress on e2e tests.
This commit is contained in:
parent
6cdb7c66e1
commit
17565f9a14
@ -20,6 +20,7 @@
|
|||||||
#include "test.h"
|
#include "test.h"
|
||||||
#include "timer.h"
|
#include "timer.h"
|
||||||
#include "threaded_queue.h"
|
#include "threaded_queue.h"
|
||||||
|
#include "work_thread.h"
|
||||||
#include "working_files.h"
|
#include "working_files.h"
|
||||||
|
|
||||||
#include <loguru.hpp>
|
#include <loguru.hpp>
|
||||||
@ -189,7 +190,7 @@ bool FindFileOrFail(QueryDatabase* db, lsRequestId id, const std::string& absolu
|
|||||||
if (out_file_id)
|
if (out_file_id)
|
||||||
*out_file_id = QueryFileId((size_t)-1);
|
*out_file_id = QueryFileId((size_t)-1);
|
||||||
|
|
||||||
LOG_S(INFO) << "Unable to find file " << absolute_path;
|
LOG_S(INFO) << "Unable to find file \"" << absolute_path << "\"";
|
||||||
|
|
||||||
Out_Error out;
|
Out_Error out;
|
||||||
out.id = id;
|
out.id = id;
|
||||||
@ -541,9 +542,10 @@ struct Index_Request {
|
|||||||
std::string path;
|
std::string path;
|
||||||
std::vector<std::string> args; // TODO: make this a string that is parsed lazily.
|
std::vector<std::string> args; // TODO: make this a string that is parsed lazily.
|
||||||
bool is_interactive;
|
bool is_interactive;
|
||||||
|
optional<std::string> contents; // Preloaded contents. Useful for tests.
|
||||||
|
|
||||||
Index_Request(const std::string& path, const std::vector<std::string>& args, bool is_interactive)
|
Index_Request(const std::string& path, const std::vector<std::string>& args, bool is_interactive, optional<std::string> contents)
|
||||||
: path(path), args(args), is_interactive(is_interactive) {}
|
: path(path), args(args), is_interactive(is_interactive), contents(contents) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Index_DoIdMap {
|
struct Index_DoIdMap {
|
||||||
@ -652,6 +654,15 @@ struct QueueManager {
|
|||||||
Index_OnIndexedQueue on_indexed;
|
Index_OnIndexedQueue on_indexed;
|
||||||
|
|
||||||
QueueManager(MultiQueueWaiter* waiter) : index_request(waiter), do_id_map(waiter), load_previous_index(waiter), on_id_mapped(waiter), on_indexed(waiter) {}
|
QueueManager(MultiQueueWaiter* waiter) : index_request(waiter), do_id_map(waiter), load_previous_index(waiter), on_id_mapped(waiter), on_indexed(waiter) {}
|
||||||
|
|
||||||
|
bool HasWork() {
|
||||||
|
return
|
||||||
|
!index_request.IsEmpty() ||
|
||||||
|
!do_id_map.IsEmpty() ||
|
||||||
|
!load_previous_index.IsEmpty() ||
|
||||||
|
!on_id_mapped.IsEmpty() ||
|
||||||
|
!on_indexed.IsEmpty();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void RegisterMessageTypes() {
|
void RegisterMessageTypes() {
|
||||||
@ -684,6 +695,9 @@ void RegisterMessageTypes() {
|
|||||||
MessageRegistry::instance()->Register<Ipc_CqueryCallers>();
|
MessageRegistry::instance()->Register<Ipc_CqueryCallers>();
|
||||||
MessageRegistry::instance()->Register<Ipc_CqueryBase>();
|
MessageRegistry::instance()->Register<Ipc_CqueryBase>();
|
||||||
MessageRegistry::instance()->Register<Ipc_CqueryDerived>();
|
MessageRegistry::instance()->Register<Ipc_CqueryDerived>();
|
||||||
|
MessageRegistry::instance()->Register<Ipc_CqueryIndexFile>();
|
||||||
|
MessageRegistry::instance()->Register<Ipc_CqueryQueryDbWaitForIdleIndexer>();
|
||||||
|
MessageRegistry::instance()->Register<Ipc_CqueryExitWhenIdle>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -726,6 +740,12 @@ struct ImportManager {
|
|||||||
import_.erase(path);
|
import_.erase(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true if there any any files currently being imported.
|
||||||
|
bool HasActiveImports() {
|
||||||
|
std::lock_guard<std::mutex> guard(mutex_);
|
||||||
|
return !import_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
std::mutex mutex_;
|
std::mutex mutex_;
|
||||||
std::unordered_set<std::string> import_;
|
std::unordered_set<std::string> import_;
|
||||||
};
|
};
|
||||||
@ -832,7 +852,8 @@ std::vector<Index_DoIdMap> DoParseFile(
|
|||||||
CacheLoader* cache_loader,
|
CacheLoader* cache_loader,
|
||||||
bool is_interactive,
|
bool is_interactive,
|
||||||
const std::string& path,
|
const std::string& path,
|
||||||
const std::vector<std::string>& args) {
|
const std::vector<std::string>& args,
|
||||||
|
const optional<FileContents>& contents) {
|
||||||
std::vector<Index_DoIdMap> result;
|
std::vector<Index_DoIdMap> result;
|
||||||
|
|
||||||
IndexFile* previous_index = cache_loader->TryLoad(path);
|
IndexFile* previous_index = cache_loader->TryLoad(path);
|
||||||
@ -904,6 +925,10 @@ std::vector<Index_DoIdMap> DoParseFile(
|
|||||||
// well. We then default to a fast file-copy if not in working set.
|
// well. We then default to a fast file-copy if not in working set.
|
||||||
bool loaded_primary = false;
|
bool loaded_primary = false;
|
||||||
std::vector<FileContents> file_contents;
|
std::vector<FileContents> file_contents;
|
||||||
|
if (contents) {
|
||||||
|
loaded_primary = loaded_primary || contents->path == path;
|
||||||
|
file_contents.push_back(*contents);
|
||||||
|
}
|
||||||
for (const auto& it : cache_loader->caches) {
|
for (const auto& it : cache_loader->caches) {
|
||||||
const std::unique_ptr<IndexFile>& index = it.second;
|
const std::unique_ptr<IndexFile>& index = it.second;
|
||||||
assert(index);
|
assert(index);
|
||||||
@ -930,6 +955,7 @@ std::vector<Index_DoIdMap> DoParseFile(
|
|||||||
config, file_consumer_shared,
|
config, file_consumer_shared,
|
||||||
path, args, file_contents,
|
path, args, file_contents,
|
||||||
&perf, index);
|
&perf, index);
|
||||||
|
// LOG_S(INFO) << "Parsing " << path << " gave " << indexes.size() << " indexes";
|
||||||
|
|
||||||
for (std::unique_ptr<IndexFile>& new_index : indexes) {
|
for (std::unique_ptr<IndexFile>& new_index : indexes) {
|
||||||
Timer time;
|
Timer time;
|
||||||
@ -964,7 +990,12 @@ std::vector<Index_DoIdMap> ParseFile(
|
|||||||
FileConsumer::SharedState* file_consumer_shared,
|
FileConsumer::SharedState* file_consumer_shared,
|
||||||
TimestampManager* timestamp_manager,
|
TimestampManager* timestamp_manager,
|
||||||
bool is_interactive,
|
bool is_interactive,
|
||||||
const Project::Entry& entry) {
|
const Project::Entry& entry,
|
||||||
|
const optional<std::string>& contents) {
|
||||||
|
|
||||||
|
optional<FileContents> file_contents;
|
||||||
|
if (contents)
|
||||||
|
file_contents = FileContents(entry.filename, *contents);
|
||||||
|
|
||||||
CacheLoader cache_loader(config);
|
CacheLoader cache_loader(config);
|
||||||
|
|
||||||
@ -973,7 +1004,7 @@ std::vector<Index_DoIdMap> ParseFile(
|
|||||||
// complain about if indexed by itself.
|
// complain about if indexed by itself.
|
||||||
IndexFile* entry_cache = cache_loader.TryLoad(entry.filename);
|
IndexFile* entry_cache = cache_loader.TryLoad(entry.filename);
|
||||||
std::string tu_path = entry_cache ? entry_cache->import_file : entry.filename;
|
std::string tu_path = entry_cache ? entry_cache->import_file : entry.filename;
|
||||||
return DoParseFile(config, working_files, index, file_consumer_shared, timestamp_manager, &cache_loader, is_interactive, tu_path, entry.args);
|
return DoParseFile(config, working_files, index, file_consumer_shared, timestamp_manager, &cache_loader, is_interactive, tu_path, entry.args, file_contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IndexMain_DoParse(
|
bool IndexMain_DoParse(
|
||||||
@ -985,18 +1016,28 @@ bool IndexMain_DoParse(
|
|||||||
TimestampManager* timestamp_manager,
|
TimestampManager* timestamp_manager,
|
||||||
clang::Index* index) {
|
clang::Index* index) {
|
||||||
|
|
||||||
optional<Index_Request> request = queue->index_request.TryDequeue();
|
bool can_import = false;
|
||||||
if (!request)
|
optional<Index_Request> request = queue->index_request.TryDequeuePlusAction([&](const Index_Request& request) {
|
||||||
return false;
|
can_import = import_manager->StartImport(request.path);
|
||||||
|
});
|
||||||
if (!import_manager->StartImport(request->path))
|
if (!request || !can_import)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Project::Entry entry;
|
Project::Entry entry;
|
||||||
entry.filename = request->path;
|
entry.filename = request->path;
|
||||||
entry.args = request->args;
|
entry.args = request->args;
|
||||||
std::vector<Index_DoIdMap> responses = ParseFile(config, working_files, index, file_consumer_shared, timestamp_manager, request->is_interactive, entry);
|
std::vector<Index_DoIdMap> responses = ParseFile(config, working_files, index, file_consumer_shared, timestamp_manager, request->is_interactive, entry, request->contents);
|
||||||
|
|
||||||
|
// Unmark file as imported if indexing failed.
|
||||||
|
bool found_import = false;
|
||||||
|
for (auto& response : responses) {
|
||||||
|
if (response.current->path == request->path)
|
||||||
|
found_import = true;
|
||||||
|
}
|
||||||
|
if (!found_import)
|
||||||
|
import_manager->DoneImport(request->path);
|
||||||
|
|
||||||
|
// Don't bother sending an IdMap request if there are no responses.
|
||||||
if (responses.empty())
|
if (responses.empty())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@ -1100,7 +1141,8 @@ bool IndexMergeIndexUpdates(QueueManager* queue) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void IndexMain(Config* config,
|
WorkThread::Result IndexMain(
|
||||||
|
Config* config,
|
||||||
FileConsumer::SharedState* file_consumer_shared,
|
FileConsumer::SharedState* file_consumer_shared,
|
||||||
ImportManager* import_manager,
|
ImportManager* import_manager,
|
||||||
TimestampManager* timestamp_manager,
|
TimestampManager* timestamp_manager,
|
||||||
@ -1108,11 +1150,9 @@ void IndexMain(Config* config,
|
|||||||
WorkingFiles* working_files,
|
WorkingFiles* working_files,
|
||||||
MultiQueueWaiter* waiter,
|
MultiQueueWaiter* waiter,
|
||||||
QueueManager* queue) {
|
QueueManager* queue) {
|
||||||
SetCurrentThreadName("indexer");
|
|
||||||
// TODO: dispose of index after it is not used for a while.
|
// TODO: dispose of index after it is not used for a while.
|
||||||
clang::Index index(1, 0);
|
clang::Index index(1, 0);
|
||||||
|
|
||||||
while (true) {
|
|
||||||
// TODO: process all off IndexMain_DoIndex before calling
|
// TODO: process all off IndexMain_DoIndex before calling
|
||||||
// IndexMain_DoCreateIndexUpdate for
|
// IndexMain_DoCreateIndexUpdate for
|
||||||
// better icache behavior. We need to have some threads spinning on
|
// better icache behavior. We need to have some threads spinning on
|
||||||
@ -1141,7 +1181,8 @@ void IndexMain(Config* config,
|
|||||||
waiter->Wait(
|
waiter->Wait(
|
||||||
{&queue->index_request, &queue->on_id_mapped, &queue->load_previous_index, &queue->on_indexed});
|
{&queue->index_request, &queue->on_id_mapped, &queue->load_previous_index, &queue->on_indexed});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return queue->HasWork() ? WorkThread::Result::MoreWork : WorkThread::Result::NoWork;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QueryDb_ImportMain(Config* config, QueryDatabase* db, ImportManager* import_manager, QueueManager* queue, WorkingFiles* working_files) {
|
bool QueryDb_ImportMain(Config* config, QueryDatabase* db, ImportManager* import_manager, QueueManager* queue, WorkingFiles* working_files) {
|
||||||
@ -1299,6 +1340,7 @@ bool QueryDb_ImportMain(Config* config, QueryDatabase* db, ImportManager* import
|
|||||||
bool QueryDbMainLoop(
|
bool QueryDbMainLoop(
|
||||||
Config* config,
|
Config* config,
|
||||||
QueryDatabase* db,
|
QueryDatabase* db,
|
||||||
|
bool* exit_when_idle,
|
||||||
MultiQueueWaiter* waiter,
|
MultiQueueWaiter* waiter,
|
||||||
QueueManager* queue,
|
QueueManager* queue,
|
||||||
Project* project,
|
Project* project,
|
||||||
@ -1336,14 +1378,15 @@ bool QueryDbMainLoop(
|
|||||||
<< " with uri " << request->params.rootUri->raw_uri;
|
<< " with uri " << request->params.rootUri->raw_uri;
|
||||||
|
|
||||||
if (!request->params.initializationOptions) {
|
if (!request->params.initializationOptions) {
|
||||||
LOG_S(INFO) << "Initialization parameters (particularily cacheDirectory) are required";
|
LOG_S(FATAL) << "Initialization parameters (particularily cacheDirectory) are required";
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
*config = *request->params.initializationOptions;
|
*config = *request->params.initializationOptions;
|
||||||
|
|
||||||
// Check client version.
|
// Check client version.
|
||||||
if (config->clientVersion != kExpectedClientVersion) {
|
if (config->clientVersion != kExpectedClientVersion &&
|
||||||
|
config->clientVersion != -1 /*disable check*/) {
|
||||||
Out_ShowLogMessage out;
|
Out_ShowLogMessage out;
|
||||||
out.display_type = Out_ShowLogMessage::DisplayType::Show;
|
out.display_type = Out_ShowLogMessage::DisplayType::Show;
|
||||||
out.params.type = lsMessageType::Error;
|
out.params.type = lsMessageType::Error;
|
||||||
@ -1375,8 +1418,8 @@ bool QueryDbMainLoop(
|
|||||||
}
|
}
|
||||||
std::cerr << "[querydb] Starting " << config->indexerCount << " indexers" << std::endl;
|
std::cerr << "[querydb] Starting " << config->indexerCount << " indexers" << std::endl;
|
||||||
for (int i = 0; i < config->indexerCount; ++i) {
|
for (int i = 0; i < config->indexerCount; ++i) {
|
||||||
new std::thread([&]() {
|
WorkThread::StartThread("indexer" + std::to_string(i), [&]() {
|
||||||
IndexMain(config, file_consumer_shared, import_manager, timestamp_manager, project, working_files, waiter, queue);
|
return IndexMain(config, file_consumer_shared, import_manager, timestamp_manager, project, working_files, waiter, queue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1395,7 +1438,7 @@ bool QueryDbMainLoop(
|
|||||||
// << "] Dispatching index request for file " << entry.filename
|
// << "] Dispatching index request for file " << entry.filename
|
||||||
// << std::endl;
|
// << std::endl;
|
||||||
bool is_interactive = working_files->GetFileByFilename(entry.filename) != nullptr;
|
bool is_interactive = working_files->GetFileByFilename(entry.filename) != nullptr;
|
||||||
queue->index_request.Enqueue(Index_Request(entry.filename, entry.args, is_interactive));
|
queue->index_request.Enqueue(Index_Request(entry.filename, entry.args, is_interactive, nullopt));
|
||||||
});
|
});
|
||||||
|
|
||||||
// We need to support multiple concurrent index processes.
|
// We need to support multiple concurrent index processes.
|
||||||
@ -1459,7 +1502,7 @@ bool QueryDbMainLoop(
|
|||||||
LOG_S(INFO) << "[" << i << "/" << (project->entries.size() - 1)
|
LOG_S(INFO) << "[" << i << "/" << (project->entries.size() - 1)
|
||||||
<< "] Dispatching index request for file " << entry.filename;
|
<< "] Dispatching index request for file " << entry.filename;
|
||||||
bool is_interactive = working_files->GetFileByFilename(entry.filename) != nullptr;
|
bool is_interactive = working_files->GetFileByFilename(entry.filename) != nullptr;
|
||||||
queue->index_request.Enqueue(Index_Request(entry.filename, entry.args, is_interactive));
|
queue->index_request.Enqueue(Index_Request(entry.filename, entry.args, is_interactive, nullopt));
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1674,7 +1717,7 @@ bool QueryDbMainLoop(
|
|||||||
|
|
||||||
// Submit new index request.
|
// Submit new index request.
|
||||||
const Project::Entry& entry = project->FindCompilationEntryForFile(path);
|
const Project::Entry& entry = project->FindCompilationEntryForFile(path);
|
||||||
queue->index_request.PriorityEnqueue(Index_Request(entry.filename, entry.args, true /*is_interactive*/));
|
queue->index_request.PriorityEnqueue(Index_Request(entry.filename, entry.args, true /*is_interactive*/, nullopt));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1719,7 +1762,7 @@ bool QueryDbMainLoop(
|
|||||||
// if so, ignore that index response.
|
// if so, ignore that index response.
|
||||||
// TODO: send as priority request
|
// TODO: send as priority request
|
||||||
Project::Entry entry = project->FindCompilationEntryForFile(path);
|
Project::Entry entry = project->FindCompilationEntryForFile(path);
|
||||||
queue->index_request.Enqueue(Index_Request(entry.filename, entry.args, true /*is_interactive*/));
|
queue->index_request.Enqueue(Index_Request(entry.filename, entry.args, true /*is_interactive*/, nullopt));
|
||||||
|
|
||||||
clang_complete->NotifySave(path);
|
clang_complete->NotifySave(path);
|
||||||
|
|
||||||
@ -2572,6 +2615,45 @@ bool QueryDbMainLoop(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case IpcId::CqueryIndexFile: {
|
||||||
|
auto msg = static_cast<Ipc_CqueryIndexFile*>(message.get());
|
||||||
|
queue->index_request.Enqueue(Index_Request(
|
||||||
|
NormalizePath(msg->params.path),
|
||||||
|
msg->params.args,
|
||||||
|
msg->params.is_interactive,
|
||||||
|
msg->params.contents));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case IpcId::CqueryQueryDbWaitForIdleIndexer: {
|
||||||
|
auto msg = static_cast<Ipc_CqueryQueryDbWaitForIdleIndexer*>(message.get());
|
||||||
|
LOG_S(INFO) << "Waiting for idle";
|
||||||
|
int idle_count = 0;
|
||||||
|
while (true) {
|
||||||
|
bool has_work = false;
|
||||||
|
has_work |= import_manager->HasActiveImports();
|
||||||
|
has_work |= queue->HasWork();
|
||||||
|
has_work |= QueryDb_ImportMain(config, db, import_manager, queue, working_files);
|
||||||
|
if (!has_work)
|
||||||
|
++idle_count;
|
||||||
|
else
|
||||||
|
idle_count = 0;
|
||||||
|
|
||||||
|
// There are race conditions between each of the three checks above,
|
||||||
|
// so we retry a bunch of times to try to avoid any.
|
||||||
|
if (idle_count > 10)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
LOG_S(INFO) << "Done waiting for idle";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case IpcId::CqueryExitWhenIdle: {
|
||||||
|
*exit_when_idle = true;
|
||||||
|
WorkThread::request_exit_on_idle = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
LOG_S(INFO) << "[querydb] Unhandled IPC message " << IpcIdToString(message->method_id);
|
LOG_S(INFO) << "[querydb] Unhandled IPC message " << IpcIdToString(message->method_id);
|
||||||
exit(1);
|
exit(1);
|
||||||
@ -2588,10 +2670,8 @@ bool QueryDbMainLoop(
|
|||||||
return did_work;
|
return did_work;
|
||||||
}
|
}
|
||||||
|
|
||||||
void QueryDbMain(const std::string& bin_name, Config* config, MultiQueueWaiter* waiter) {
|
void RunQueryDbThread(const std::string& bin_name, Config* config, MultiQueueWaiter* waiter, QueueManager* queue) {
|
||||||
// Create queues.
|
bool exit_when_idle = false;
|
||||||
QueueManager queue(waiter);
|
|
||||||
|
|
||||||
Project project;
|
Project project;
|
||||||
WorkingFiles working_files;
|
WorkingFiles working_files;
|
||||||
ClangCompleteManager clang_complete(
|
ClangCompleteManager clang_complete(
|
||||||
@ -2610,18 +2690,22 @@ void QueryDbMain(const std::string& bin_name, Config* config, MultiQueueWaiter*
|
|||||||
QueryDatabase db;
|
QueryDatabase db;
|
||||||
while (true) {
|
while (true) {
|
||||||
bool did_work = QueryDbMainLoop(
|
bool did_work = QueryDbMainLoop(
|
||||||
config, &db, waiter, &queue,
|
config, &db, &exit_when_idle, waiter, queue,
|
||||||
&project, &file_consumer_shared, &import_manager, ×tamp_manager, &working_files,
|
&project, &file_consumer_shared, &import_manager, ×tamp_manager, &working_files,
|
||||||
&clang_complete, &include_complete, global_code_complete_cache.get(), non_global_code_complete_cache.get(), signature_cache.get());
|
&clang_complete, &include_complete, global_code_complete_cache.get(), non_global_code_complete_cache.get(), signature_cache.get());
|
||||||
|
|
||||||
|
// No more work left and exit request. Exit.
|
||||||
|
if (!did_work && exit_when_idle && WorkThread::num_active_threads == 0)
|
||||||
|
exit(0);
|
||||||
|
|
||||||
// Cleanup and free any unused memory.
|
// Cleanup and free any unused memory.
|
||||||
FreeUnusedMemory();
|
FreeUnusedMemory();
|
||||||
|
|
||||||
if (!did_work) {
|
if (!did_work) {
|
||||||
waiter->Wait({
|
waiter->Wait({
|
||||||
IpcManager::instance()->threaded_queue_for_server_.get(),
|
IpcManager::instance()->threaded_queue_for_server_.get(),
|
||||||
&queue.do_id_map,
|
&queue->do_id_map,
|
||||||
&queue.on_indexed
|
&queue->on_indexed
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2688,8 +2772,6 @@ void QueryDbMain(const std::string& bin_name, Config* config, MultiQueueWaiter*
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: global lock on stderr output.
|
|
||||||
|
|
||||||
// Separate thread whose only job is to read from stdin and
|
// Separate thread whose only job is to read from stdin and
|
||||||
// dispatch read commands to the actual indexer program. This
|
// dispatch read commands to the actual indexer program. This
|
||||||
@ -2697,16 +2779,16 @@ void QueryDbMain(const std::string& bin_name, Config* config, MultiQueueWaiter*
|
|||||||
// blocks.
|
// blocks.
|
||||||
//
|
//
|
||||||
// |ipc| is connected to a server.
|
// |ipc| is connected to a server.
|
||||||
void LanguageServerStdinLoop(Config* config, std::unordered_map<IpcId, Timer>* request_times) {
|
void LaunchStdinLoop(Config* config, std::unordered_map<IpcId, Timer>* request_times) {
|
||||||
|
|
||||||
|
WorkThread::StartThread("stdin", [config, request_times]() {
|
||||||
IpcManager* ipc = IpcManager::instance();
|
IpcManager* ipc = IpcManager::instance();
|
||||||
|
|
||||||
SetCurrentThreadName("stdin");
|
|
||||||
while (true) {
|
|
||||||
std::unique_ptr<BaseIpcMessage> message = MessageRegistry::instance()->ReadMessageFromStdin();
|
std::unique_ptr<BaseIpcMessage> message = MessageRegistry::instance()->ReadMessageFromStdin();
|
||||||
|
|
||||||
// Message parsing can fail if we don't recognize the method.
|
// Message parsing can fail if we don't recognize the method.
|
||||||
if (!message)
|
if (!message)
|
||||||
continue;
|
return WorkThread::Result::MoreWork;
|
||||||
|
|
||||||
(*request_times)[message->method_id] = Timer();
|
(*request_times)[message->method_id] = Timer();
|
||||||
|
|
||||||
@ -2722,8 +2804,21 @@ void LanguageServerStdinLoop(Config* config, std::unordered_map<IpcId, Timer>* r
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case IpcId::Exit: {
|
||||||
|
exit(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case IpcId::CqueryExitWhenIdle: {
|
||||||
|
// querydb needs to know to exit when idle. We return out of the stdin
|
||||||
|
// loop to exit the thread. If we keep parsing input stdin is likely
|
||||||
|
// closed so cquery will exit.
|
||||||
|
LOG_S(INFO) << "cquery will exit when all threads are idle";
|
||||||
|
ipc->SendMessage(IpcManager::Destination::Server, std::move(message));
|
||||||
|
return WorkThread::Result::ExitThread;
|
||||||
|
}
|
||||||
|
|
||||||
case IpcId::Initialize:
|
case IpcId::Initialize:
|
||||||
case IpcId::Exit:
|
|
||||||
case IpcId::TextDocumentDidOpen:
|
case IpcId::TextDocumentDidOpen:
|
||||||
case IpcId::TextDocumentDidChange:
|
case IpcId::TextDocumentDidChange:
|
||||||
case IpcId::TextDocumentDidClose:
|
case IpcId::TextDocumentDidClose:
|
||||||
@ -2747,7 +2842,9 @@ void LanguageServerStdinLoop(Config* config, std::unordered_map<IpcId, Timer>* r
|
|||||||
case IpcId::CqueryVars:
|
case IpcId::CqueryVars:
|
||||||
case IpcId::CqueryCallers:
|
case IpcId::CqueryCallers:
|
||||||
case IpcId::CqueryBase:
|
case IpcId::CqueryBase:
|
||||||
case IpcId::CqueryDerived: {
|
case IpcId::CqueryDerived:
|
||||||
|
case IpcId::CqueryIndexFile:
|
||||||
|
case IpcId::CqueryQueryDbWaitForIdleIndexer: {
|
||||||
ipc->SendMessage(IpcManager::Destination::Server, std::move(message));
|
ipc->SendMessage(IpcManager::Destination::Server, std::move(message));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -2757,7 +2854,9 @@ void LanguageServerStdinLoop(Config* config, std::unordered_map<IpcId, Timer>* r
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return WorkThread::Result::MoreWork;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -2805,15 +2904,14 @@ void LanguageServerStdinLoop(Config* config, std::unordered_map<IpcId, Timer>* r
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
void StdoutMain(std::unordered_map<IpcId, Timer>* request_times, MultiQueueWaiter* waiter) {
|
void LaunchStdoutThread(std::unordered_map<IpcId, Timer>* request_times, MultiQueueWaiter* waiter, QueueManager* queue) {
|
||||||
SetCurrentThreadName("stdout");
|
WorkThread::StartThread("stdout", [=]() {
|
||||||
IpcManager* ipc = IpcManager::instance();
|
IpcManager* ipc = IpcManager::instance();
|
||||||
|
|
||||||
while (true) {
|
|
||||||
std::vector<std::unique_ptr<BaseIpcMessage>> messages = ipc->GetMessages(IpcManager::Destination::Client);
|
std::vector<std::unique_ptr<BaseIpcMessage>> messages = ipc->GetMessages(IpcManager::Destination::Client);
|
||||||
if (messages.empty()) {
|
if (messages.empty()) {
|
||||||
waiter->Wait({ipc->threaded_queue_for_client_.get()});
|
waiter->Wait({ ipc->threaded_queue_for_client_.get() });
|
||||||
continue;
|
return queue->HasWork() ? WorkThread::Result::MoreWork : WorkThread::Result::NoWork;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& message : messages) {
|
for (auto& message : messages) {
|
||||||
@ -2839,26 +2937,24 @@ void StdoutMain(std::unordered_map<IpcId, Timer>* request_times, MultiQueueWaite
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return WorkThread::Result::MoreWork;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void LanguageServerMain(const std::string& bin_name, Config* config, MultiQueueWaiter* waiter) {
|
void LanguageServerMain(const std::string& bin_name, Config* config, MultiQueueWaiter* waiter) {
|
||||||
|
QueueManager queue(waiter);
|
||||||
std::unordered_map<IpcId, Timer> request_times;
|
std::unordered_map<IpcId, Timer> request_times;
|
||||||
|
|
||||||
// Start stdin reader. Reading from stdin is a blocking operation so this
|
LaunchStdinLoop(config, &request_times);
|
||||||
// needs a dedicated thread.
|
|
||||||
new std::thread([&]() {
|
|
||||||
LanguageServerStdinLoop(config, &request_times);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start querydb thread. querydb will start indexer threads as needed.
|
|
||||||
new std::thread([&]() {
|
|
||||||
QueryDbMain(bin_name, config, waiter);
|
|
||||||
});
|
|
||||||
|
|
||||||
// We run a dedicated thread for writing to stdout because there can be an
|
// We run a dedicated thread for writing to stdout because there can be an
|
||||||
// unknown number of delays when output information.
|
// unknown number of delays when output information.
|
||||||
StdoutMain(&request_times, waiter);
|
LaunchStdoutThread(&request_times, waiter, &queue);
|
||||||
|
|
||||||
|
// Start querydb which takes over this thread. The querydb will launch
|
||||||
|
// indexer threads as needed.
|
||||||
|
RunQueryDbThread(bin_name, config, waiter, &queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
#include "project.h"
|
#include "project.h"
|
||||||
#include "standard_includes.h"
|
#include "standard_includes.h"
|
||||||
#include "timer.h"
|
#include "timer.h"
|
||||||
|
#include "work_thread.h"
|
||||||
|
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
@ -114,8 +115,7 @@ void IncludeComplete::Rescan() {
|
|||||||
match_ = MakeUnique<GroupMatch>(config_->includeCompletionWhitelist, config_->includeCompletionBlacklist);
|
match_ = MakeUnique<GroupMatch>(config_->includeCompletionWhitelist, config_->includeCompletionBlacklist);
|
||||||
|
|
||||||
is_scanning = true;
|
is_scanning = true;
|
||||||
new std::thread([this]() {
|
WorkThread::StartThread("include_scanner", [this]() {
|
||||||
SetCurrentThreadName("include_scanner");
|
|
||||||
Timer timer;
|
Timer timer;
|
||||||
|
|
||||||
InsertStlIncludes();
|
InsertStlIncludes();
|
||||||
@ -127,6 +127,8 @@ void IncludeComplete::Rescan() {
|
|||||||
|
|
||||||
timer.ResetAndPrint("[perf] Scanning for includes");
|
timer.ResetAndPrint("[perf] Scanning for includes");
|
||||||
is_scanning = false;
|
is_scanning = false;
|
||||||
|
|
||||||
|
return WorkThread::Result::ExitThread;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +71,14 @@ const char* IpcIdToString(IpcId id) {
|
|||||||
|
|
||||||
case IpcId::Cout:
|
case IpcId::Cout:
|
||||||
return "$cout";
|
return "$cout";
|
||||||
|
|
||||||
|
case IpcId::CqueryIndexFile:
|
||||||
|
return "$cquery/indexFile";
|
||||||
|
case IpcId::CqueryQueryDbWaitForIdleIndexer:
|
||||||
|
return "$cquery/queryDbWaitForIdleIndexer";
|
||||||
|
case IpcId::CqueryExitWhenIdle:
|
||||||
|
return "$cquery/exitWhenIdle";
|
||||||
|
|
||||||
default:
|
default:
|
||||||
assert(false && "missing IpcId string name");
|
assert(false && "missing IpcId string name");
|
||||||
exit(1);
|
exit(1);
|
||||||
|
33
src/ipc.h
33
src/ipc.h
@ -46,7 +46,14 @@ enum class IpcId : int {
|
|||||||
CqueryDerived, // Show all derived types/methods.
|
CqueryDerived, // Show all derived types/methods.
|
||||||
|
|
||||||
// Internal implementation detail.
|
// Internal implementation detail.
|
||||||
Cout
|
Cout,
|
||||||
|
|
||||||
|
// Index the given file contents. Used in tests.
|
||||||
|
CqueryIndexFile,
|
||||||
|
// Make querydb wait for the indexer to be idle. Used in tests.
|
||||||
|
CqueryQueryDbWaitForIdleIndexer,
|
||||||
|
// Exit after all messages have been read/processes. Used in tests.
|
||||||
|
CqueryExitWhenIdle
|
||||||
};
|
};
|
||||||
MAKE_ENUM_HASHABLE(IpcId)
|
MAKE_ENUM_HASHABLE(IpcId)
|
||||||
MAKE_REFLECT_TYPE_PROXY(IpcId, int)
|
MAKE_REFLECT_TYPE_PROXY(IpcId, int)
|
||||||
@ -69,3 +76,27 @@ struct Ipc_Cout : public IpcMessage<Ipc_Cout> {
|
|||||||
IpcId original_ipc_id;
|
IpcId original_ipc_id;
|
||||||
};
|
};
|
||||||
MAKE_REFLECT_STRUCT(Ipc_Cout, content);
|
MAKE_REFLECT_STRUCT(Ipc_Cout, content);
|
||||||
|
|
||||||
|
struct Ipc_CqueryIndexFile : public IpcMessage<Ipc_CqueryIndexFile> {
|
||||||
|
static constexpr IpcId kIpcId = IpcId::CqueryIndexFile;
|
||||||
|
|
||||||
|
struct Params {
|
||||||
|
std::string path;
|
||||||
|
std::vector<std::string> args;
|
||||||
|
bool is_interactive = false;
|
||||||
|
std::string contents;
|
||||||
|
};
|
||||||
|
Params params;
|
||||||
|
};
|
||||||
|
MAKE_REFLECT_STRUCT(Ipc_CqueryIndexFile::Params, path, args, is_interactive, contents);
|
||||||
|
MAKE_REFLECT_STRUCT(Ipc_CqueryIndexFile, params);
|
||||||
|
|
||||||
|
struct Ipc_CqueryQueryDbWaitForIdleIndexer : public IpcMessage<Ipc_CqueryQueryDbWaitForIdleIndexer> {
|
||||||
|
static constexpr IpcId kIpcId = IpcId::CqueryQueryDbWaitForIdleIndexer;
|
||||||
|
};
|
||||||
|
MAKE_REFLECT_EMPTY_STRUCT(Ipc_CqueryQueryDbWaitForIdleIndexer);
|
||||||
|
|
||||||
|
struct Ipc_CqueryExitWhenIdle : public IpcMessage<Ipc_CqueryExitWhenIdle> {
|
||||||
|
static constexpr IpcId kIpcId = IpcId::CqueryExitWhenIdle;
|
||||||
|
};
|
||||||
|
MAKE_REFLECT_EMPTY_STRUCT(Ipc_CqueryExitWhenIdle);
|
@ -1,5 +1,8 @@
|
|||||||
#include "language_server_api.h"
|
#include "language_server_api.h"
|
||||||
|
|
||||||
|
#include <doctest/doctest.h>
|
||||||
|
#include <loguru.hpp>
|
||||||
|
|
||||||
void Reflect(Writer& visitor, lsRequestId& value) {
|
void Reflect(Writer& visitor, lsRequestId& value) {
|
||||||
assert(value.id0.has_value() || value.id1.has_value());
|
assert(value.id0.has_value() || value.id1.has_value());
|
||||||
|
|
||||||
@ -22,55 +25,113 @@ void Reflect(Reader& visitor, lsRequestId& id) {
|
|||||||
|
|
||||||
MessageRegistry* MessageRegistry::instance_ = nullptr;
|
MessageRegistry* MessageRegistry::instance_ = nullptr;
|
||||||
|
|
||||||
std::unique_ptr<BaseIpcMessage> MessageRegistry::ReadMessageFromStdin() {
|
// Reads a JsonRpc message. |read| returns the next input character.
|
||||||
int content_length = -1;
|
optional<std::string> ReadJsonRpcContentFrom(std::function<optional<char>()> read) {
|
||||||
int iteration = 0;
|
// Read the content length. It is terminated by the "\r\n" sequence.
|
||||||
|
int exit_seq = 0;
|
||||||
|
std::string stringified_content_length;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (++iteration > 10) {
|
optional<char> opt_c = read();
|
||||||
assert(false && "bad parser state");
|
if (!opt_c) {
|
||||||
|
LOG_S(INFO) << "No more input when reading content length header";
|
||||||
|
return nullopt;
|
||||||
|
}
|
||||||
|
char c = *opt_c;
|
||||||
|
|
||||||
|
if (exit_seq == 0 && c == '\r') ++exit_seq;
|
||||||
|
if (exit_seq == 1 && c == '\n') break;
|
||||||
|
|
||||||
|
stringified_content_length += c;
|
||||||
|
}
|
||||||
|
constexpr char* kContentLengthStart = "Content-Length: ";
|
||||||
|
assert(StartsWith(stringified_content_length, kContentLengthStart));
|
||||||
|
int content_length = atoi(stringified_content_length.c_str() + strlen(kContentLengthStart));
|
||||||
|
|
||||||
|
// There is always a "\r\n" sequence before the actual content.
|
||||||
|
auto expect_char = [&](char expected) {
|
||||||
|
optional<char> opt_c = read();
|
||||||
|
return opt_c && *opt_c == expected;
|
||||||
|
};
|
||||||
|
if (!expect_char('\r') || !expect_char('\n')) {
|
||||||
|
LOG_S(INFO) << "Unexpected token (expected \r\n sequence)";
|
||||||
|
return nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read content.
|
||||||
|
std::string content;
|
||||||
|
content.reserve(content_length);
|
||||||
|
for (size_t i = 0; i < content_length; ++i) {
|
||||||
|
optional<char> c = read();
|
||||||
|
if (!c) {
|
||||||
|
LOG_S(INFO) << "No more input when reading content body";
|
||||||
|
return nullopt;
|
||||||
|
}
|
||||||
|
content += *c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_SUITE("FindIncludeLine");
|
||||||
|
|
||||||
|
auto MakeContentReader(std::string* content, bool can_be_empty) {
|
||||||
|
return [=]() -> optional<char> {
|
||||||
|
if (!can_be_empty)
|
||||||
|
REQUIRE(!content->empty());
|
||||||
|
if (content->empty())
|
||||||
|
return nullopt;
|
||||||
|
char c = (*content)[0];
|
||||||
|
content->erase(content->begin());
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ReadContentFromSource") {
|
||||||
|
auto parse_correct = [](std::string content) {
|
||||||
|
auto reader = MakeContentReader(&content, false /*can_be_empty*/);
|
||||||
|
auto got = ReadJsonRpcContentFrom(reader);
|
||||||
|
REQUIRE(got);
|
||||||
|
return got.value();
|
||||||
|
};
|
||||||
|
|
||||||
|
auto parse_incorrect = [](std::string content) {
|
||||||
|
auto reader = MakeContentReader(&content, true /*can_be_empty*/);
|
||||||
|
return ReadJsonRpcContentFrom(reader);
|
||||||
|
};
|
||||||
|
|
||||||
|
REQUIRE(parse_correct("Content-Length: 0\r\n\r\n") == "");
|
||||||
|
REQUIRE(parse_correct("Content-Length: 1\r\n\r\na") == "a");
|
||||||
|
REQUIRE(parse_correct("Content-Length: 4\r\n\r\nabcd") == "abcd");
|
||||||
|
|
||||||
|
REQUIRE(parse_incorrect("ggg") == optional<std::string>());
|
||||||
|
REQUIRE(parse_incorrect("Content-Length: 0\r\n") == optional<std::string>());
|
||||||
|
REQUIRE(parse_incorrect("Content-Length: 5\r\n\r\nab") == optional<std::string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_SUITE_END();
|
||||||
|
|
||||||
|
optional<char> ReadCharFromStdinBlocking() {
|
||||||
|
// Bad stdin means parent process has probably exited. Either way, cquery
|
||||||
|
// can no longer be communicated with so just exit.
|
||||||
|
if (!std::cin.good()) {
|
||||||
|
LOG_S(FATAL) << "std::cin.good() is false; exiting";
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string line;
|
char c = 0;
|
||||||
std::getline(std::cin, line);
|
|
||||||
|
|
||||||
// No content; end of stdin.
|
|
||||||
if (line.empty()) {
|
|
||||||
std::cerr << "stdin closed; exiting" << std::endl;
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// std::cin >> line;
|
|
||||||
// std::cerr << "Read line " << line;
|
|
||||||
|
|
||||||
if (line.compare(0, 14, "Content-Length") == 0) {
|
|
||||||
content_length = atoi(line.c_str() + 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line == "\r")
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// bad input that is not a message.
|
|
||||||
if (content_length < 0) {
|
|
||||||
std::cerr << "parsing command failed (no Content-Length header)"
|
|
||||||
<< std::endl;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: maybe use std::cin.read(c, content_length)
|
|
||||||
std::string content;
|
|
||||||
content.reserve(content_length);
|
|
||||||
for (int i = 0; i < content_length; ++i) {
|
|
||||||
char c;
|
|
||||||
std::cin.read(&c, 1);
|
std::cin.read(&c, 1);
|
||||||
content += c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
//std::cerr << content.c_str() << std::endl;
|
std::unique_ptr<BaseIpcMessage> MessageRegistry::ReadMessageFromStdin() {
|
||||||
|
optional<std::string> content = ReadJsonRpcContentFrom(&ReadCharFromStdinBlocking);
|
||||||
|
if (!content) {
|
||||||
|
LOG_S(FATAL) << "Failed to read JsonRpc input; exiting";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
rapidjson::Document document;
|
rapidjson::Document document;
|
||||||
document.Parse(content.c_str(), content_length);
|
document.Parse(content->c_str(), content->length());
|
||||||
assert(!document.HasParseError());
|
assert(!document.HasParseError());
|
||||||
|
|
||||||
return Parse(document);
|
return Parse(document);
|
||||||
@ -86,7 +147,7 @@ std::unique_ptr<BaseIpcMessage> MessageRegistry::Parse(Reader& visitor) {
|
|||||||
ReflectMember(visitor, "method", method);
|
ReflectMember(visitor, "method", method);
|
||||||
|
|
||||||
if (allocators.find(method) == allocators.end()) {
|
if (allocators.find(method) == allocators.end()) {
|
||||||
std::cerr << "Unable to find registered handler for method \"" << method << "\"" << std::endl;
|
LOG_S(ERROR) << "Unable to find registered handler for method \"" << method << "\"" << std::endl;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "work_thread.h"
|
||||||
#include <optional.h>
|
#include <optional.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@ -38,8 +39,11 @@ struct MultiQueueWaiter {
|
|||||||
// HasState() is called data gets posted but before we begin waiting for
|
// HasState() is called data gets posted but before we begin waiting for
|
||||||
// the condition variable, we will miss the notification. The timeout of 5
|
// the condition variable, we will miss the notification. The timeout of 5
|
||||||
// means that if this happens we will delay operation for 5 seconds.
|
// means that if this happens we will delay operation for 5 seconds.
|
||||||
|
//
|
||||||
|
// If we're trying to exit (WorkThread::request_exit_on_idle), do not
|
||||||
|
// bother waiting.
|
||||||
|
|
||||||
while (!HasState(queues)) {
|
while (!HasState(queues) && !WorkThread::request_exit_on_idle) {
|
||||||
std::unique_lock<std::mutex> l(m);
|
std::unique_lock<std::mutex> l(m);
|
||||||
cv.wait_for(l, std::chrono::seconds(5));
|
cv.wait_for(l, std::chrono::seconds(5));
|
||||||
}
|
}
|
||||||
@ -132,7 +136,8 @@ public:
|
|||||||
|
|
||||||
// Get the first element from the queue without blocking. Returns a null
|
// Get the first element from the queue without blocking. Returns a null
|
||||||
// value if the queue is empty.
|
// value if the queue is empty.
|
||||||
optional<T> TryDequeue() {
|
template<typename TAction>
|
||||||
|
optional<T> TryDequeuePlusAction(TAction action) {
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
if (priority_.empty() && queue_.empty())
|
if (priority_.empty() && queue_.empty())
|
||||||
return nullopt;
|
return nullopt;
|
||||||
@ -145,9 +150,16 @@ public:
|
|||||||
|
|
||||||
auto val = std::move(queue_.front());
|
auto val = std::move(queue_.front());
|
||||||
queue_.pop();
|
queue_.pop();
|
||||||
|
|
||||||
|
action(val);
|
||||||
|
|
||||||
return std::move(val);
|
return std::move(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
optional<T> TryDequeue() {
|
||||||
|
return TryDequeuePlusAction([](const T&) {});
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::queue<T> priority_;
|
std::queue<T> priority_;
|
||||||
mutable std::mutex mutex_;
|
mutable std::mutex mutex_;
|
||||||
|
28
src/work_thread.cc
Normal file
28
src/work_thread.cc
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#include "work_thread.h"
|
||||||
|
|
||||||
|
#include "platform.h"
|
||||||
|
|
||||||
|
std::atomic<int> WorkThread::num_active_threads;
|
||||||
|
std::atomic<bool> WorkThread::request_exit_on_idle;
|
||||||
|
|
||||||
|
// static
|
||||||
|
void WorkThread::StartThread(
|
||||||
|
const std::string& thread_name,
|
||||||
|
const std::function<Result()>& entry_point) {
|
||||||
|
new std::thread([thread_name, entry_point]() {
|
||||||
|
SetCurrentThreadName(thread_name);
|
||||||
|
|
||||||
|
++num_active_threads;
|
||||||
|
|
||||||
|
// Main loop.
|
||||||
|
while (true) {
|
||||||
|
Result result = entry_point();
|
||||||
|
if (result == Result::ExitThread)
|
||||||
|
break;
|
||||||
|
if (request_exit_on_idle && result == Result::NoWork)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
--num_active_threads;
|
||||||
|
});
|
||||||
|
}
|
31
src/work_thread.h
Normal file
31
src/work_thread.h
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Helper methods for starting threads that do some work. Enables test code to
|
||||||
|
// wait for all work to complete.
|
||||||
|
struct WorkThread {
|
||||||
|
enum class Result {
|
||||||
|
MoreWork,
|
||||||
|
NoWork,
|
||||||
|
ExitThread
|
||||||
|
};
|
||||||
|
|
||||||
|
// The number of active worker threads.
|
||||||
|
static std::atomic<int> num_active_threads;
|
||||||
|
// Set to true to request all work thread instances to exit.
|
||||||
|
static std::atomic<bool> request_exit_on_idle;
|
||||||
|
|
||||||
|
// Launch a new thread. |entry_point| will be called continously. It should
|
||||||
|
// return true if it there is still known work to be done.
|
||||||
|
static void StartThread(
|
||||||
|
const std::string& thread_name,
|
||||||
|
const std::function<Result()>& entry_point);
|
||||||
|
|
||||||
|
// Static-only class.
|
||||||
|
WorkThread() = delete;
|
||||||
|
};
|
@ -1,7 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
|
|
||||||
|
|
||||||
|
CQUERY_PATH = 'x64/Debug/cquery.exe'
|
||||||
|
CACHE_DIR = 'e2e_CACHE'
|
||||||
|
|
||||||
# Content-Length: ...\r\n
|
# Content-Length: ...\r\n
|
||||||
# \r\n
|
# \r\n
|
||||||
# {
|
# {
|
||||||
@ -22,15 +27,33 @@ from subprocess import Popen, PIPE
|
|||||||
|
|
||||||
class TestBuilder:
|
class TestBuilder:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.files = []
|
|
||||||
self.sent = []
|
self.sent = []
|
||||||
self.received = []
|
self.expected = []
|
||||||
|
|
||||||
def WithFile(self, filename, contents):
|
def IndexFile(self, path, contents):
|
||||||
"""
|
"""
|
||||||
Writes the file contents to disk so that the language server can access it.
|
Writes the file contents to disk so that the language server can access it.
|
||||||
"""
|
"""
|
||||||
self.files.append((filename, contents))
|
self.Send({
|
||||||
|
'method': '$cquery/indexFile',
|
||||||
|
'params': {
|
||||||
|
'path': path,
|
||||||
|
'contents': contents,
|
||||||
|
'args': [
|
||||||
|
'-xc++',
|
||||||
|
'-std=c++11',
|
||||||
|
'-isystemC:/Program Files (x86)/Microsoft Visual Studio/2017/Community/VC/Tools/MSVC/14.10.25017/include',
|
||||||
|
'-isystemC:/Program Files (x86)/Windows Kits/10/Include/10.0.15063.0/ucrt'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return self
|
||||||
|
|
||||||
|
def WaitForIdle(self):
|
||||||
|
"""
|
||||||
|
Blocks the querydb thread until any active imports are complete.
|
||||||
|
"""
|
||||||
|
self.Send({'method': '$cquery/queryDbWaitForIdleIndexer'})
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def Send(self, stdin):
|
def Send(self, stdin):
|
||||||
@ -41,11 +64,12 @@ class TestBuilder:
|
|||||||
self.sent.append(stdin)
|
self.sent.append(stdin)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def Expect(self, stdout):
|
def Expect(self, expected):
|
||||||
"""
|
"""
|
||||||
Expect a message from the language server.
|
Expect a message from the language server.
|
||||||
"""
|
"""
|
||||||
self.received.append(stdout)
|
expected['jsonrpc'] = '2.0'
|
||||||
|
self.expected.append(expected)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def SetupCommonInit(self):
|
def SetupCommonInit(self):
|
||||||
@ -57,15 +81,45 @@ class TestBuilder:
|
|||||||
'method': 'initialize',
|
'method': 'initialize',
|
||||||
'params': {
|
'params': {
|
||||||
'processId': 123,
|
'processId': 123,
|
||||||
'rootPath': 'cquery',
|
'rootUri': 'cquery',
|
||||||
'capabilities': {},
|
'capabilities': {},
|
||||||
'trace': 'off'
|
'trace': 'off',
|
||||||
|
'initializationOptions': {
|
||||||
|
'cacheDirectory': CACHE_DIR,
|
||||||
|
'clientVersion': -1 # Disables the check
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
self.Expect({
|
self.Expect({
|
||||||
'id': 0,
|
'id': 0,
|
||||||
'method': 'initialized',
|
'result': {
|
||||||
'result': {}
|
'capabilities': {
|
||||||
|
'textDocumentSync': 2,
|
||||||
|
'hoverProvider': True,
|
||||||
|
'completionProvider': {
|
||||||
|
'resolveProvider': False,
|
||||||
|
'triggerCharacters': [ '.', ':', '>', '#' ]
|
||||||
|
},
|
||||||
|
'signatureHelpProvider': {
|
||||||
|
'triggerCharacters': [ '(', ',' ]
|
||||||
|
},
|
||||||
|
'definitionProvider': True,
|
||||||
|
'referencesProvider': True,
|
||||||
|
'documentHighlightProvider': True,
|
||||||
|
'documentSymbolProvider': True,
|
||||||
|
'workspaceSymbolProvider': True,
|
||||||
|
'codeActionProvider': True,
|
||||||
|
'codeLensProvider': {
|
||||||
|
'resolveProvider': False
|
||||||
|
},
|
||||||
|
'documentFormattingProvider': False,
|
||||||
|
'documentRangeFormattingProvider': False,
|
||||||
|
'renameProvider': True,
|
||||||
|
'documentLinkProvider': {
|
||||||
|
'resolveProvider': False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -79,32 +133,76 @@ def _ExecuteTest(name, func):
|
|||||||
if not isinstance(test_builder, TestBuilder):
|
if not isinstance(test_builder, TestBuilder):
|
||||||
raise Exception('%s does not return a TestBuilder instance' % name)
|
raise Exception('%s does not return a TestBuilder instance' % name)
|
||||||
|
|
||||||
test_builder.Send({ 'method': 'exit' })
|
# Add a final exit message.
|
||||||
|
test_builder.Send({ 'method': '$cquery/exitWhenIdle' })
|
||||||
# Possible test runner implementation
|
|
||||||
cmd = "x64/Debug/indexer.exe --language-server"
|
|
||||||
process = Popen(shlex.split(cmd), stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True)
|
|
||||||
|
|
||||||
|
# Convert messages to a stdin byte array.
|
||||||
stdin = ''
|
stdin = ''
|
||||||
for message in test_builder.sent:
|
for message in test_builder.sent:
|
||||||
payload = json.dumps(message)
|
payload = json.dumps(message)
|
||||||
wrapped = 'Content-Length: %s\r\n\r\n%s' % (len(payload), payload)
|
wrapped = 'Content-Length: %s\r\n\r\n%s' % (len(payload), payload)
|
||||||
stdin += wrapped
|
stdin += wrapped
|
||||||
|
stdin_bytes = stdin.encode(encoding='UTF-8')
|
||||||
|
|
||||||
print('## %s ##' % name)
|
# Finds all messages in |string| by parsing Content-Length headers.
|
||||||
print('== STDIN ==')
|
def GetMessages(string):
|
||||||
print(stdin)
|
messages = []
|
||||||
(stdout, stderr) = process.communicate(stdin)
|
for match in re.finditer('Content-Length: (\d+)\r\n\r\n', string):
|
||||||
|
start = match.span()[1]
|
||||||
|
length = int(match.groups()[0])
|
||||||
|
message = string[start:start + length]
|
||||||
|
messages.append(json.loads(message))
|
||||||
|
return messages
|
||||||
|
|
||||||
|
# Utility method to print a byte array.
|
||||||
|
def PrintByteArray(bytes):
|
||||||
|
for line in bytes.split(b'\r\n'):
|
||||||
|
print(line.decode('utf8'))
|
||||||
|
|
||||||
|
# Execute program.
|
||||||
|
cmd = "%s --language-server" % CQUERY_PATH
|
||||||
|
process = Popen(shlex.split(cmd), stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||||
|
(stdout, stderr) = process.communicate(stdin_bytes)
|
||||||
|
exit_code = process.wait();
|
||||||
|
|
||||||
|
# Check if test succeeded.
|
||||||
|
actual = GetMessages(stdout.decode('utf8'))
|
||||||
|
success = actual == test_builder.expected
|
||||||
|
|
||||||
|
# Print failure messages.
|
||||||
|
if success:
|
||||||
|
print('== Passed %s with exit_code=%s ==' % (name, exit_code))
|
||||||
|
else:
|
||||||
|
print('== FAILED %s with exit_code=%s ==' % (name, exit_code))
|
||||||
|
print('## STDIN:')
|
||||||
|
for message in GetMessages(stdin):
|
||||||
|
print(json.dumps(message, indent=True))
|
||||||
if stdout:
|
if stdout:
|
||||||
print('== STDOUT ==')
|
print('## STDOUT:')
|
||||||
print(stdout)
|
for message in GetMessages(stdout.decode('utf8')):
|
||||||
|
print(json.dumps(message, indent=True))
|
||||||
if stderr:
|
if stderr:
|
||||||
print('== STDERR ==')
|
print('## STDERR:')
|
||||||
print(stderr)
|
PrintByteArray(stderr)
|
||||||
|
|
||||||
# TODO: Actually verify stdout.
|
print('## Expected output')
|
||||||
|
for message in test_builder.expected:
|
||||||
|
print(message)
|
||||||
|
print('## Actual output')
|
||||||
|
for message in actual:
|
||||||
|
print(message)
|
||||||
|
print('## Difference')
|
||||||
|
common_end = min(len(test_builder.expected), len(actual))
|
||||||
|
for i in range(0, common_end):
|
||||||
|
if test_builder.expected[i] != actual[i]:
|
||||||
|
print('i=%s' % i)
|
||||||
|
print('- Expected %s' % str(test_builder.expected[i]))
|
||||||
|
print('- Actual %s' % str(actual[i]))
|
||||||
|
for i in range(common_end, len(test_builder.expected)):
|
||||||
|
print('Extra expected: %s' % str(test_builder.expected[i]))
|
||||||
|
for i in range(common_end, len(actual)):
|
||||||
|
print('Extra actual: %s' % str(actual[i]))
|
||||||
|
|
||||||
exit_code = process.wait()
|
|
||||||
|
|
||||||
def _DiscoverTests():
|
def _DiscoverTests():
|
||||||
"""
|
"""
|
||||||
@ -122,12 +220,16 @@ def _RunTests():
|
|||||||
Executes all tests.
|
Executes all tests.
|
||||||
"""
|
"""
|
||||||
for name, func in _DiscoverTests():
|
for name, func in _DiscoverTests():
|
||||||
print('Running test function %s' % name)
|
|
||||||
_ExecuteTest(name, func)
|
_ExecuteTest(name, func)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### EXAMPLE TESTS ####
|
||||||
|
|
||||||
|
|
||||||
class lsSymbolKind:
|
class lsSymbolKind:
|
||||||
Function = 1
|
Function = 1
|
||||||
|
|
||||||
@ -138,24 +240,32 @@ def lsSymbolInfo(name, position, kind):
|
|||||||
'kind': kind
|
'kind': kind
|
||||||
}
|
}
|
||||||
|
|
||||||
def Test_Init():
|
def DISABLED_Test_Init():
|
||||||
return (TestBuilder()
|
return (TestBuilder()
|
||||||
.SetupCommonInit()
|
.SetupCommonInit()
|
||||||
)
|
)
|
||||||
|
|
||||||
def _Test_Outline():
|
def Test_Outline():
|
||||||
return (TestBuilder()
|
return (TestBuilder()
|
||||||
.SetupCommonInit()
|
.SetupCommonInit()
|
||||||
.WithFile("foo.cc",
|
# .IndexFile("file:///C%3A/Users/jacob/Desktop/cquery/foo.cc",
|
||||||
"""
|
.IndexFile("foo.cc",
|
||||||
void main() {}
|
"""void foobar();""")
|
||||||
"""
|
.WaitForIdle()
|
||||||
)
|
|
||||||
.Send({
|
.Send({
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'method': 'textDocument/documentSymbol',
|
'method': 'textDocument/documentSymbol',
|
||||||
'params': {}
|
'params': {
|
||||||
|
'textDocument': {
|
||||||
|
'uri': 'C:/Users/jacob/Desktop/cquery/foo.cc'
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
# .Expect({
|
||||||
|
# 'jsonrpc': '2.0',
|
||||||
|
# 'id': 1,
|
||||||
|
# 'error': {'code': -32603, 'message': 'Unable to find file '}
|
||||||
|
# }))
|
||||||
.Expect({
|
.Expect({
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'result': [
|
'result': [
|
||||||
|
Loading…
Reference in New Issue
Block a user