blob: 8c11b3935c2698cbfe3996aafd5c69392b9ce002 [file]
/* 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
});
}