/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt or https://cmake.org/licensing for details. */ #include <chrono> #include <cstdio> #include <functional> #include <future> #include <memory> #include <string> #include <vector> #include <cm3p/cppdap/future.h> #include <cm3p/cppdap/io.h> #include <cm3p/cppdap/optional.h> #include <cm3p/cppdap/protocol.h> #include <cm3p/cppdap/session.h> #include <cm3p/cppdap/types.h> #include "cmDebuggerAdapter.h" #include "cmDebuggerProtocol.h" #include "cmVersionConfig.h" #include "testCommon.h" #include "testDebugger.h" class DebuggerLocalConnection : public cmDebugger::cmDebuggerConnection { public: DebuggerLocalConnection() : ClientToDebugger(dap::pipe()) , DebuggerToClient(dap::pipe()) { } bool StartListening(std::string& errorMessage) override { errorMessage = ""; return true; } void WaitForConnection() override {} std::shared_ptr<dap::Reader> GetReader() override { return ClientToDebugger; }; std::shared_ptr<dap::Writer> GetWriter() override { return DebuggerToClient; } std::shared_ptr<dap::ReaderWriter> ClientToDebugger; std::shared_ptr<dap::ReaderWriter> DebuggerToClient; }; bool runTest(std::function<bool(dap::Session&)> onThreadExitedEvent) { std::promise<bool> debuggerAdapterInitializedPromise; std::future<bool> debuggerAdapterInitializedFuture = debuggerAdapterInitializedPromise.get_future(); std::promise<bool> initializedEventReceivedPromise; std::future<bool> initializedEventReceivedFuture = initializedEventReceivedPromise.get_future(); std::promise<bool> exitedEventReceivedPromise; std::future<bool> exitedEventReceivedFuture = exitedEventReceivedPromise.get_future(); std::promise<bool> terminatedEventReceivedPromise; std::future<bool> terminatedEventReceivedFuture = terminatedEventReceivedPromise.get_future(); std::promise<bool> threadStartedPromise; std::future<bool> threadStartedFuture = threadStartedPromise.get_future(); std::promise<bool> threadExitedPromise; std::future<bool> threadExitedFuture = threadExitedPromise.get_future(); std::promise<bool> disconnectResponseReceivedPromise; std::future<bool> disconnectResponseReceivedFuture = disconnectResponseReceivedPromise.get_future(); auto futureTimeout = std::chrono::seconds(60); auto connection = std::make_shared<DebuggerLocalConnection>(); std::unique_ptr<dap::Session> client = dap::Session::create(); client->registerHandler([&](const dap::InitializedEvent& e) { (void)e; initializedEventReceivedPromise.set_value(true); }); client->registerHandler([&](const dap::ExitedEvent& e) { (void)e; exitedEventReceivedPromise.set_value(true); }); client->registerHandler([&](const dap::TerminatedEvent& e) { (void)e; terminatedEventReceivedPromise.set_value(true); }); client->registerHandler([&](const dap::ThreadEvent& e) { if (e.reason == "started") { threadStartedPromise.set_value(true); } else if (e.reason == "exited") { threadExitedPromise.set_value(true); } }); client->bind(connection->DebuggerToClient, connection->ClientToDebugger); ScopedThread debuggerThread([&]() -> int { std::shared_ptr<cmDebugger::cmDebuggerAdapter> debuggerAdapter = std::make_shared<cmDebugger::cmDebuggerAdapter>( connection, dap::file(stdout, false)); debuggerAdapterInitializedPromise.set_value(true); debuggerAdapter->ReportExitCode(0); // Ensure the disconnectResponse has been received before // destructing debuggerAdapter. ASSERT_TRUE(disconnectResponseReceivedFuture.wait_for(futureTimeout) == std::future_status::ready); return 0; }); dap::CMakeInitializeRequest initializeRequest; auto initializeResponse = client->send(initializeRequest).get(); ASSERT_TRUE(initializeResponse.response.cmakeVersion.full == CMake_VERSION); ASSERT_TRUE(initializeResponse.response.cmakeVersion.major == CMake_VERSION_MAJOR); ASSERT_TRUE(initializeResponse.response.cmakeVersion.minor == CMake_VERSION_MINOR); ASSERT_TRUE(initializeResponse.response.cmakeVersion.patch == CMake_VERSION_PATCH); ASSERT_TRUE(initializeResponse.response.supportsExceptionInfoRequest); ASSERT_TRUE( initializeResponse.response.exceptionBreakpointFilters.has_value()); dap::LaunchRequest launchRequest; auto launchResponse = client->send(launchRequest).get(); ASSERT_TRUE(!launchResponse.error); dap::ConfigurationDoneRequest configurationDoneRequest; auto configurationDoneResponse = client->send(configurationDoneRequest).get(); ASSERT_TRUE(!configurationDoneResponse.error); ASSERT_TRUE(debuggerAdapterInitializedFuture.wait_for(futureTimeout) == std::future_status::ready); ASSERT_TRUE(initializedEventReceivedFuture.wait_for(futureTimeout) == std::future_status::ready); ASSERT_TRUE(threadStartedFuture.wait_for(futureTimeout) == std::future_status::ready); ASSERT_TRUE(threadExitedFuture.wait_for(futureTimeout) == std::future_status::ready); if (onThreadExitedEvent) { ASSERT_TRUE(onThreadExitedEvent(*client)); } ASSERT_TRUE(exitedEventReceivedFuture.wait_for(futureTimeout) == std::future_status::ready); ASSERT_TRUE(terminatedEventReceivedFuture.wait_for(futureTimeout) == std::future_status::ready); dap::DisconnectRequest disconnectRequest; auto disconnectResponse = client->send(disconnectRequest).get(); disconnectResponseReceivedPromise.set_value(true); ASSERT_TRUE(!disconnectResponse.error); return true; } bool testBasicProtocol() { return runTest(nullptr); } bool testThreadsRequestAfterThreadExitedEvent() { return runTest([](dap::Session& session) -> bool { // Try requesting threads again after receiving the thread exited event. // Some clients do this to ensure that their thread list is up-to-date. dap::ThreadsRequest threadsRequest; auto threadsResponse = session.send(threadsRequest).get(); ASSERT_TRUE(!threadsResponse.error); // CMake only has one DAP thread. Once that thread exits, there should be // no threads left. ASSERT_TRUE(threadsResponse.response.threads.empty()); return true; }); } int testDebuggerAdapter(int, char*[]) { return runTests(std::vector<std::function<bool()>>{ testBasicProtocol, testThreadsRequestAfterThreadExitedEvent, }); }