| /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying | |
| file LICENSE.rst or https://cmake.org/licensing for details. */ | |
| #include <atomic> | |
| #include <chrono> | |
| #include <future> | |
| #include <memory> | |
| #include <string> | |
| #include <vector> | |
| #include <cm3p/cppdap/future.h> | |
| #include <cm3p/cppdap/optional.h> | |
| #include <cm3p/cppdap/protocol.h> | |
| #include <cm3p/cppdap/session.h> | |
| #include <cm3p/cppdap/types.h> | |
| #include "cmDebuggerBreakpointManager.h" | |
| #include "cmDebuggerSourceBreakpoint.h" // IWYU pragma: keep | |
| #if defined(_WIN32) || defined(__APPLE__) | |
| # include "cmsys/FStream.hxx" | |
| # include "cmSystemTools.h" | |
| # include "testConfig.h" | |
| #endif | |
| #include "testCommon.h" | |
| #include "testDebugger.h" | |
| class cmListFileFunction; | |
| // Fictional absolute path used by the pre-existing breakpoint tests. | |
| // Must be absolute on all platforms so that ToNormalizedPathOnDisk | |
| // (used inside the breakpoint manager) does not resolve it relative | |
| // to the working directory. | |
| #ifdef _WIN32 | |
| static std::string const kTestSourcePath = "C:/CMakeLists.txt"; | |
| #else | |
| static std::string const kTestSourcePath = "/CMakeLists.txt"; | |
| #endif | |
| static bool testHandleBreakpointRequestBeforeFileIsLoaded() | |
| { | |
| // Arrange | |
| DebuggerTestHelper helper; | |
| cmDebugger::cmDebuggerBreakpointManager breakpointManager( | |
| helper.Debugger.get()); | |
| helper.bind(); | |
| dap::SetBreakpointsRequest setBreakpointRequest; | |
| std::string sourcePath = kTestSourcePath; | |
| setBreakpointRequest.source.path = sourcePath; | |
| dap::array<dap::SourceBreakpoint> sourceBreakpoints(3); | |
| sourceBreakpoints[0].line = 1; | |
| sourceBreakpoints[1].line = 2; | |
| sourceBreakpoints[2].line = 3; | |
| setBreakpointRequest.breakpoints = sourceBreakpoints; | |
| // Act | |
| auto got = helper.Client->send(setBreakpointRequest).get(); | |
| // Assert | |
| auto& response = got.response; | |
| ASSERT_TRUE(!got.error); | |
| ASSERT_TRUE(response.breakpoints.size() == sourceBreakpoints.size()); | |
| ASSERT_BREAKPOINT(response.breakpoints[0], 0, sourceBreakpoints[0].line, | |
| sourcePath, false); | |
| ASSERT_BREAKPOINT(response.breakpoints[1], 1, sourceBreakpoints[1].line, | |
| sourcePath, false); | |
| ASSERT_BREAKPOINT(response.breakpoints[2], 2, sourceBreakpoints[2].line, | |
| sourcePath, false); | |
| ASSERT_TRUE(breakpointManager.GetBreakpointCount() == 3); | |
| // setBreakpoints should override any existing breakpoints | |
| setBreakpointRequest.breakpoints.value().clear(); | |
| helper.Client->send(setBreakpointRequest).get(); | |
| ASSERT_TRUE(breakpointManager.GetBreakpointCount() == 0); | |
| return true; | |
| } | |
| static bool testHandleBreakpointRequestAfterFileIsLoaded() | |
| { | |
| // Arrange | |
| DebuggerTestHelper helper; | |
| std::atomic<bool> notExpectBreakpointEvents(true); | |
| helper.Client->registerHandler([&](dap::BreakpointEvent const&) { | |
| notExpectBreakpointEvents.store(false); | |
| }); | |
| cmDebugger::cmDebuggerBreakpointManager breakpointManager( | |
| helper.Debugger.get()); | |
| helper.bind(); | |
| std::string sourcePath = kTestSourcePath; | |
| std::vector<cmListFileFunction> functions = helper.CreateListFileFunctions( | |
| "# Comment1\nset(var1 foo)\n# Comment2\nset(var2\nbar)\n", sourcePath); | |
| breakpointManager.SourceFileLoaded(sourcePath, functions); | |
| dap::SetBreakpointsRequest setBreakpointRequest; | |
| setBreakpointRequest.source.path = sourcePath; | |
| dap::array<dap::SourceBreakpoint> sourceBreakpoints(5); | |
| sourceBreakpoints[0].line = 1; | |
| sourceBreakpoints[1].line = 2; | |
| sourceBreakpoints[2].line = 3; | |
| sourceBreakpoints[3].line = 4; | |
| sourceBreakpoints[4].line = 5; | |
| setBreakpointRequest.breakpoints = sourceBreakpoints; | |
| // Act | |
| auto got = helper.Client->send(setBreakpointRequest).get(); | |
| // Assert | |
| auto& response = got.response; | |
| ASSERT_TRUE(!got.error); | |
| ASSERT_TRUE(response.breakpoints.size() == sourceBreakpoints.size()); | |
| // Line 1 is a comment. Move it to next valid function, which is line 2. | |
| ASSERT_BREAKPOINT(response.breakpoints[0], 0, 2, sourcePath, true); | |
| ASSERT_BREAKPOINT(response.breakpoints[1], 1, sourceBreakpoints[1].line, | |
| sourcePath, true); | |
| // Line 3 is a comment. Move it to next valid function, which is line 4. | |
| ASSERT_BREAKPOINT(response.breakpoints[2], 2, 4, sourcePath, true); | |
| ASSERT_BREAKPOINT(response.breakpoints[3], 3, sourceBreakpoints[3].line, | |
| sourcePath, true); | |
| // Line 5 is the 2nd part of line 4 function. No valid function after line 5, | |
| // show the breakpoint at line 4. | |
| ASSERT_BREAKPOINT(response.breakpoints[4], 4, sourceBreakpoints[3].line, | |
| sourcePath, true); | |
| ASSERT_TRUE(notExpectBreakpointEvents.load()); | |
| ASSERT_TRUE(breakpointManager.GetBreakpointCount() == 5); | |
| // setBreakpoints should override any existing breakpoints | |
| setBreakpointRequest.breakpoints.value().clear(); | |
| helper.Client->send(setBreakpointRequest).get(); | |
| ASSERT_TRUE(breakpointManager.GetBreakpointCount() == 0); | |
| return true; | |
| } | |
| static bool testSourceFileLoadedAfterHandleBreakpointRequest() | |
| { | |
| // Arrange | |
| DebuggerTestHelper helper; | |
| std::vector<dap::BreakpointEvent> breakpointEvents; | |
| std::atomic<int> remainingBreakpointEvents(5); | |
| std::promise<void> allBreakpointEventsReceivedPromise; | |
| std::future<void> allBreakpointEventsReceivedFuture = | |
| allBreakpointEventsReceivedPromise.get_future(); | |
| helper.Client->registerHandler([&](dap::BreakpointEvent const& event) { | |
| breakpointEvents.emplace_back(event); | |
| if (--remainingBreakpointEvents == 0) { | |
| allBreakpointEventsReceivedPromise.set_value(); | |
| } | |
| }); | |
| cmDebugger::cmDebuggerBreakpointManager breakpointManager( | |
| helper.Debugger.get()); | |
| helper.bind(); | |
| dap::SetBreakpointsRequest setBreakpointRequest; | |
| std::string sourcePath = kTestSourcePath; | |
| setBreakpointRequest.source.path = sourcePath; | |
| dap::array<dap::SourceBreakpoint> sourceBreakpoints(5); | |
| sourceBreakpoints[0].line = 1; | |
| sourceBreakpoints[1].line = 2; | |
| sourceBreakpoints[2].line = 3; | |
| sourceBreakpoints[3].line = 4; | |
| sourceBreakpoints[4].line = 5; | |
| setBreakpointRequest.breakpoints = sourceBreakpoints; | |
| std::vector<cmListFileFunction> functions = helper.CreateListFileFunctions( | |
| "# Comment1\nset(var1 foo)\n# Comment2\nset(var2\nbar)\n", sourcePath); | |
| auto got = helper.Client->send(setBreakpointRequest).get(); | |
| // Act | |
| breakpointManager.SourceFileLoaded(sourcePath, functions); | |
| ASSERT_TRUE(allBreakpointEventsReceivedFuture.wait_for( | |
| std::chrono::seconds(10)) == std::future_status::ready); | |
| // Assert | |
| ASSERT_TRUE(breakpointEvents.size() > 0); | |
| // Line 1 is a comment. Move it to next valid function, which is line 2. | |
| ASSERT_BREAKPOINT(breakpointEvents[0].breakpoint, 0, 2, sourcePath, true); | |
| ASSERT_BREAKPOINT(breakpointEvents[1].breakpoint, 1, | |
| sourceBreakpoints[1].line, sourcePath, true); | |
| // Line 3 is a comment. Move it to next valid function, which is line 4. | |
| ASSERT_BREAKPOINT(breakpointEvents[2].breakpoint, 2, 4, sourcePath, true); | |
| ASSERT_BREAKPOINT(breakpointEvents[3].breakpoint, 3, | |
| sourceBreakpoints[3].line, sourcePath, true); | |
| // Line 5 is the 2nd part of line 4 function. No valid function after line 5, | |
| // show the breakpoint at line 4. | |
| ASSERT_BREAKPOINT(breakpointEvents[4].breakpoint, 4, | |
| sourceBreakpoints[3].line, sourcePath, true); | |
| return true; | |
| } | |
| #if defined(_WIN32) || defined(__APPLE__) | |
| // RAII guard for temp directory cleanup on both success and assertion failure. | |
| struct TempDirGuard | |
| { | |
| std::string Path; | |
| ~TempDirGuard() | |
| { | |
| if (!Path.empty()) { | |
| cmSystemTools::RemoveADirectory(Path); | |
| } | |
| } | |
| }; | |
| static bool testBreakpointCaseMismatch_SourceLoadedFirst() | |
| { | |
| std::string tempDir = std::string(BUILD_DIR) + "/cmakedbg-bp1-XXXXXX"; | |
| if (!cmSystemTools::MakeTempDirectory(tempDir)) { | |
| std::cout << "Failed to create temp directory\n"; | |
| return false; | |
| } | |
| TempDirGuard guard; | |
| guard.Path = tempDir; | |
| std::string createdFile = tempDir + "/TestCase.cmake"; | |
| { | |
| cmsys::ofstream f(createdFile.c_str()); | |
| } | |
| std::string canonicalPath = | |
| cmSystemTools::ToNormalizedPathOnDisk(createdFile); | |
| std::string lowerFile = tempDir + "/testcase.cmake"; | |
| std::string normalizedLower = | |
| cmSystemTools::ToNormalizedPathOnDisk(lowerFile); | |
| // Skip when ToNormalizedPathOnDisk does not correct on-disk case: | |
| // a case-sensitive filesystem, or CYGWIN (treated as POSIX, no probe). | |
| if (normalizedLower != canonicalPath) { | |
| std::cout << "Skipping: ToNormalizedPathOnDisk does not correct case\n"; | |
| return true; | |
| } | |
| // Arrange | |
| DebuggerTestHelper helper; | |
| std::atomic<bool> noBreakpointEvents(true); | |
| helper.Client->registerHandler( | |
| [&](dap::BreakpointEvent const&) { noBreakpointEvents.store(false); }); | |
| cmDebugger::cmDebuggerBreakpointManager breakpointManager( | |
| helper.Debugger.get()); | |
| helper.bind(); | |
| std::vector<cmListFileFunction> functions = | |
| helper.CreateListFileFunctions("set(var1 foo)\n", canonicalPath); | |
| // Act: load source with lowercase path, set breakpoints with uppercase. | |
| breakpointManager.SourceFileLoaded(lowerFile, functions); | |
| dap::SetBreakpointsRequest req; | |
| req.source.path = tempDir + "/TESTCASE.CMAKE"; | |
| dap::array<dap::SourceBreakpoint> bps(1); | |
| bps[0].line = 1; | |
| req.breakpoints = bps; | |
| auto got = helper.Client->send(req).get(); | |
| // Assert: breakpoint verified because both paths normalize to canonicalPath. | |
| ASSERT_TRUE(!got.error); | |
| ASSERT_TRUE(got.response.breakpoints.size() == 1); | |
| ASSERT_BREAKPOINT(got.response.breakpoints[0], 0, 1, canonicalPath, true); | |
| ASSERT_TRUE(noBreakpointEvents.load()); | |
| ASSERT_TRUE(breakpointManager.GetBreakpointCount() == 1); | |
| return true; | |
| } | |
| static bool testBreakpointCaseMismatch_BreakpointsSetFirst() | |
| { | |
| std::string tempDir = std::string(BUILD_DIR) + "/cmakedbg-bp2-XXXXXX"; | |
| if (!cmSystemTools::MakeTempDirectory(tempDir)) { | |
| std::cout << "Failed to create temp directory\n"; | |
| return false; | |
| } | |
| TempDirGuard guard; | |
| guard.Path = tempDir; | |
| std::string createdFile = tempDir + "/TestCase.cmake"; | |
| { | |
| cmsys::ofstream f(createdFile.c_str()); | |
| } | |
| std::string canonicalPath = | |
| cmSystemTools::ToNormalizedPathOnDisk(createdFile); | |
| std::string lowerFile = tempDir + "/testcase.cmake"; | |
| std::string normalizedLower = | |
| cmSystemTools::ToNormalizedPathOnDisk(lowerFile); | |
| // Skip when ToNormalizedPathOnDisk does not correct on-disk case: | |
| // a case-sensitive filesystem, or CYGWIN (treated as POSIX, no probe). | |
| if (normalizedLower != canonicalPath) { | |
| std::cout << "Skipping: ToNormalizedPathOnDisk does not correct case\n"; | |
| return true; | |
| } | |
| // Arrange | |
| DebuggerTestHelper helper; | |
| std::vector<dap::BreakpointEvent> breakpointEvents; | |
| std::atomic<int> remainingEvents(1); | |
| std::promise<void> allEventsPromise; | |
| std::future<void> allEventsFuture = allEventsPromise.get_future(); | |
| helper.Client->registerHandler([&](dap::BreakpointEvent const& event) { | |
| breakpointEvents.emplace_back(event); | |
| if (--remainingEvents == 0) { | |
| allEventsPromise.set_value(); | |
| } | |
| }); | |
| cmDebugger::cmDebuggerBreakpointManager breakpointManager( | |
| helper.Debugger.get()); | |
| helper.bind(); | |
| // Act: set breakpoints with uppercase path before file is loaded. | |
| dap::SetBreakpointsRequest req; | |
| req.source.path = tempDir + "/TESTCASE.CMAKE"; | |
| dap::array<dap::SourceBreakpoint> bps(1); | |
| bps[0].line = 1; | |
| req.breakpoints = bps; | |
| auto got = helper.Client->send(req).get(); | |
| ASSERT_TRUE(!got.error); | |
| ASSERT_TRUE(got.response.breakpoints.size() == 1); | |
| ASSERT_BREAKPOINT(got.response.breakpoints[0], 0, 1, canonicalPath, false); | |
| // Act: load source with lowercase path. | |
| std::vector<cmListFileFunction> functions = | |
| helper.CreateListFileFunctions("set(var1 foo)\n", canonicalPath); | |
| breakpointManager.SourceFileLoaded(lowerFile, functions); | |
| ASSERT_TRUE(allEventsFuture.wait_for(std::chrono::seconds(10)) == | |
| std::future_status::ready); | |
| // Assert: breakpoint event fires with verified status. | |
| ASSERT_TRUE(breakpointEvents.size() == 1); | |
| ASSERT_BREAKPOINT(breakpointEvents[0].breakpoint, 0, 1, canonicalPath, true); | |
| return true; | |
| } | |
| static bool testGetBreakpoints_CaseMismatch() | |
| { | |
| std::string tempDir = std::string(BUILD_DIR) + "/cmakedbg-bp3-XXXXXX"; | |
| if (!cmSystemTools::MakeTempDirectory(tempDir)) { | |
| std::cout << "Failed to create temp directory\n"; | |
| return false; | |
| } | |
| TempDirGuard guard; | |
| guard.Path = tempDir; | |
| std::string createdFile = tempDir + "/TestCase.cmake"; | |
| { | |
| cmsys::ofstream f(createdFile.c_str()); | |
| } | |
| std::string canonicalPath = | |
| cmSystemTools::ToNormalizedPathOnDisk(createdFile); | |
| std::string lowerFile = tempDir + "/testcase.cmake"; | |
| std::string normalizedLower = | |
| cmSystemTools::ToNormalizedPathOnDisk(lowerFile); | |
| // Skip when ToNormalizedPathOnDisk does not correct on-disk case: | |
| // a case-sensitive filesystem, or CYGWIN (treated as POSIX, no probe). | |
| if (normalizedLower != canonicalPath) { | |
| std::cout << "Skipping: ToNormalizedPathOnDisk does not correct case\n"; | |
| return true; | |
| } | |
| // Arrange | |
| DebuggerTestHelper helper; | |
| cmDebugger::cmDebuggerBreakpointManager breakpointManager( | |
| helper.Debugger.get()); | |
| helper.bind(); | |
| std::vector<cmListFileFunction> functions = | |
| helper.CreateListFileFunctions("set(var1 foo)\n", canonicalPath); | |
| breakpointManager.SourceFileLoaded(canonicalPath, functions); | |
| dap::SetBreakpointsRequest req; | |
| req.source.path = canonicalPath; | |
| dap::array<dap::SourceBreakpoint> bps(1); | |
| bps[0].line = 1; | |
| req.breakpoints = bps; | |
| auto got = helper.Client->send(req).get(); | |
| ASSERT_TRUE(!got.error); | |
| ASSERT_TRUE(got.response.breakpoints.size() == 1); | |
| ASSERT_BREAKPOINT(got.response.breakpoints[0], 0, 1, canonicalPath, true); | |
| // Act: query breakpoints with a different-case path. | |
| auto breakpoints = | |
| breakpointManager.GetBreakpoints(tempDir + "/TESTCASE.CMAKE", 1); | |
| // Assert: found despite case mismatch. | |
| ASSERT_TRUE(breakpoints.size() == 1); | |
| ASSERT_TRUE(breakpoints[0] == 0); | |
| return true; | |
| } | |
| #endif | |
| int testDebuggerBreakpointManager(int, char*[]) | |
| { | |
| return runTests({ | |
| testHandleBreakpointRequestBeforeFileIsLoaded, | |
| testHandleBreakpointRequestAfterFileIsLoaded, | |
| testSourceFileLoadedAfterHandleBreakpointRequest, | |
| #if defined(_WIN32) || defined(__APPLE__) | |
| testBreakpointCaseMismatch_SourceLoadedFirst, | |
| testBreakpointCaseMismatch_BreakpointsSetFirst, | |
| testGetBreakpoints_CaseMismatch, | |
| #endif | |
| }); | |
| } |