mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
The existing tests use "C:/CMakeLists.txt", which is only absolute on Windows. ToNormalizedPathOnDisk resolves relative paths against the working directory, so give the tests a platform-appropriate absolute path via kTestSourcePath. Add three tests that set breakpoints using a different letter case than the path passed to SourceFileLoaded or GetBreakpoints, covering all three NormalizePath call sites. The tests create a real file on disk so that ToNormalizedPathOnDisk can resolve the canonical case, and skip gracefully on platforms where ToNormalizedPathOnDisk does not correct on-disk case (case-sensitive filesystems, or CYGWIN which is treated as POSIX).
414 lines
14 KiB
C++
414 lines
14 KiB
C++
/* 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
|
|
});
|
|
}
|