| /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying | |
| file LICENSE.rst or https://cmake.org/licensing for details. */ | |
| #include <chrono> | |
| #include <cstdio> | |
| #include <future> | |
| #include <memory> | |
| #include <string> | |
| #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([&](dap::InitializedEvent /*unused*/) { | |
| initializedEventReceivedPromise.set_value(true); | |
| }); | |
| client->registerHandler([&](dap::ExitedEvent /*unused*/) { | |
| exitedEventReceivedPromise.set_value(true); | |
| }); | |
| client->registerHandler([&](dap::TerminatedEvent const& /*unused*/) { | |
| terminatedEventReceivedPromise.set_value(true); | |
| }); | |
| client->registerHandler([&](dap::ThreadEvent const& 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()); | |
| ASSERT_TRUE(initializeResponse.response.supportsValueFormattingOptions); | |
| 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( | |
| { testBasicProtocol, testThreadsRequestAfterThreadExitedEvent }); | |
| } |