From 3bb1eea7f9e212e0bae0b5ae7cc1318ff4537217 Mon Sep 17 00:00:00 2001 From: Ben Clayton Date: Tue, 11 Feb 2020 12:44:03 +0000 Subject: [PATCH] Debugger: Add support for EXPECT CALLSTACK Checks the callstack is as expected. Note: I've updated the .amber scripts with the current SwiftShader output. It's clear that this is somewhat wonky, and will change. --- docs/debugger.md | 13 ++- src/amberscript/parser.cc | 31 +++++- src/debug.cc | 4 + src/debug.h | 17 ++- src/vulkan/engine_vulkan_debugger.cc | 101 ++++++++++++++++-- tests/cases/debugger_hlsl_line_stepping.amber | 4 + .../cases/debugger_spirv_line_stepping.amber | 6 ++ 7 files changed, 161 insertions(+), 15 deletions(-) diff --git a/docs/debugger.md b/docs/debugger.md index 6af0aa0d8..6c3208b78 100644 --- a/docs/debugger.md +++ b/docs/debugger.md @@ -70,7 +70,18 @@ Within each `THREAD` command block, you may use any of the following commands to * `EXPECT LOCATION` \ \ [\] Verifies that the debugger is currently paused at the given line location. - The [\] is an additional, optional check that verifies the line of the file reported by the debuggeer is as expected. + The [\] is an additional, optional check that verifies the line of the file reported by the debugger is as expected. + +* `EXPECT CALLSTACK` + + Verifies that the debugger is currently paused with the given complete stack frame. + Each frame must be declared on a separate line, starting with the most nested call, and has the form: + + \ [\ [\]] + + The [\] and [\] fields are additional, optionals checks that verify the file and line numbers reported by the debugger for the frame are as expected. + + The list of stack frames is terminated with `END`. * `EXPECT LOCAL` \ `EQ` [\] diff --git a/src/amberscript/parser.cc b/src/amberscript/parser.cc index 7d0368afd..31bafffc5 100644 --- a/src/amberscript/parser.cc +++ b/src/amberscript/parser.cc @@ -1932,7 +1932,6 @@ Result Parser::ParseDebugThreadBody(debug::Thread* thread) { } thread->ExpectLocation(location, line_source); - } else if (token->AsString() == "LOCAL") { auto name = tokenizer_->NextToken(); if (!name->IsString()) { @@ -1953,7 +1952,37 @@ Result Parser::ParseDebugThreadBody(debug::Thread* thread) { } else { return Result("expected variable value"); } + } else if (token->AsString() == "CALLSTACK") { + std::vector stack; + for (auto tok = tokenizer_->NextToken(); tok->AsString() != "END"; + tok = tokenizer_->NextToken()) { + if (tok->IsEOL()) { + continue; + } + debug::StackFrame frame; + if (!tok->IsString()) { + return Result("expected stack frame name"); + } + frame.name = tok->AsString(); + + tok = tokenizer_->NextToken(); + if (tok->IsString()) { + frame.location.file = tok->AsString(); + tok = tokenizer_->NextToken(); + if (tok->IsInteger()) { + frame.location.line = tok->AsUint32(); + } else if (!tok->IsEOL()) { + return Result( + "expected end of line or stack frame location number"); + } + } else if (!tok->IsEOL()) { + return Result( + "expected end of line or stack frame location file name"); + } + stack.emplace_back(frame); + } + thread->ExpectCallstack(stack); } else { return Result("expected LOCATION or LOCAL"); } diff --git a/src/debug.cc b/src/debug.cc index eb45c22e6..a8f07c073 100644 --- a/src/debug.cc +++ b/src/debug.cc @@ -96,6 +96,10 @@ class ThreadScriptImpl : public ThreadScript { [=](Thread* t) { t->ExpectLocation(location, line); }); } + void ExpectCallstack(const std::vector& callstack) override { + sequence_.emplace_back([=](Thread* t) { t->ExpectCallstack(callstack); }); + } + void ExpectLocal(const std::string& name, int64_t value) override { sequence_.emplace_back([=](Thread* t) { t->ExpectLocal(name, value); }); } diff --git a/src/debug.h b/src/debug.h index 86dd51565..25d062583 100644 --- a/src/debug.h +++ b/src/debug.h @@ -20,6 +20,7 @@ #include #include #include +#include #include "amber/result.h" @@ -32,8 +33,14 @@ class ThreadScript; /// Location holds a file path and a 1-based line number. struct Location { - std::string file; - uint32_t line; + std::string file; // empty represents unspecified. + uint32_t line = 0; // 0 represents unspecified. +}; + +// StackFrame holds name and location of a stack frame. +struct StackFrame { + std::string name; + Location location; }; /// Thread is the interface used to control a single debugger thread of @@ -66,6 +73,12 @@ class Thread { virtual void ExpectLocation(const Location& location, const std::string& line) = 0; + /// ExpectCallstack verifies that the debugger is currently suspended for the + /// given thread of execution with the specified callstack. + /// callstack is ordered with the 0th element representing the most nested + /// call. + virtual void ExpectCallstack(const std::vector& callstack) = 0; + /// ExpectLocal verifies that the local variable with the given name has the /// expected value. |name| may contain `.` delimiters to index structure or /// array types. diff --git a/src/vulkan/engine_vulkan_debugger.cc b/src/vulkan/engine_vulkan_debugger.cc index 295545cde..432436791 100644 --- a/src/vulkan/engine_vulkan_debugger.cc +++ b/src/vulkan/engine_vulkan_debugger.cc @@ -218,9 +218,20 @@ class Client { // TopStackFrame retrieves the frame at the top of the thread's call stack. // Returns true on success, false on error. - bool TopStackFrame(dap::integer threadId, dap::StackFrame* frame) { + bool TopStackFrame(dap::integer thread_id, dap::StackFrame* frame) { + std::vector stack; + if (!Callstack(thread_id, &stack)) { + return false; + } + *frame = stack.front(); + return true; + } + + // Callstack retrieves the thread's full call stack. + // Returns true on success, false on error. + bool Callstack(dap::integer thread_id, std::vector* stack) { dap::StackTraceRequest request; - request.threadId = threadId; + request.threadId = thread_id; auto response = session_->send(request).get(); if (response.error) { onerror_(response.error.message); @@ -230,7 +241,7 @@ class Client { onerror_("Stack frame is empty"); return false; } - *frame = response.response.stackFrames.front(); + *stack = response.response.stackFrames; return true; } @@ -519,7 +530,7 @@ class Thread : public debug::Thread { int threadId, int lane, std::shared_ptr script) - : threadId_(threadId), + : thread_id_(threadId), lane_(lane), client_(session, [this](const std::string& err) { OnError(err); }) { // The thread script runs concurrently with other debugger thread scripts. @@ -549,28 +560,28 @@ class Thread : public debug::Thread { void StepOver() override { DEBUGGER_LOG("StepOver()"); dap::NextRequest request; - request.threadId = threadId_; + request.threadId = thread_id_; client_.Send(request); } void StepIn() override { DEBUGGER_LOG("StepIn()"); dap::StepInRequest request; - request.threadId = threadId_; + request.threadId = thread_id_; client_.Send(request); } void StepOut() override { DEBUGGER_LOG("StepOut()"); dap::StepOutRequest request; - request.threadId = threadId_; + request.threadId = thread_id_; client_.Send(request); } void Continue() override { DEBUGGER_LOG("Continue()"); dap::ContinueRequest request; - request.threadId = threadId_; + request.threadId = thread_id_; client_.Send(request); } @@ -580,7 +591,7 @@ class Thread : public debug::Thread { location.line); dap::StackFrame frame; - if (!client_.TopStackFrame(threadId_, &frame)) { + if (!client_.TopStackFrame(thread_id_, &frame)) { return; } @@ -608,6 +619,53 @@ class Thread : public debug::Thread { } } + void ExpectCallstack( + const std::vector& callstack) override { + DEBUGGER_LOG("ExpectCallstack()"); + + std::vector got_stack; + if (!client_.Callstack(thread_id_, &got_stack)) { + return; + } + + std::stringstream ss; + + size_t count = std::min(callstack.size(), got_stack.size()); + for (size_t i = 0; i < count; i++) { + auto const& got_frame = got_stack[i]; + auto const& want_frame = callstack[i]; + bool ok = got_frame.name == want_frame.name; + if (ok && want_frame.location.file != "") { + ok = got_frame.source.has_value() && + got_frame.source->name.value("") == want_frame.location.file; + } + if (ok && want_frame.location.line != 0) { + ok = got_frame.line == static_cast(want_frame.location.line); + } + if (!ok) { + ss << "Unexpected stackframe at frame " << i + << "\nGot: " << FrameString(got_frame) + << "\nExpected: " << FrameString(want_frame) << "\n"; + } + } + + if (got_stack.size() > callstack.size()) { + ss << "Callstack has an additional " + << (got_stack.size() - callstack.size()) << " unexpected frames\n"; + } else if (callstack.size() > got_stack.size()) { + ss << "Callstack is missing " << (callstack.size() - got_stack.size()) + << " frames\n"; + } + + if (ss.str().size() > 0) { + ss << "Full callstack:\n"; + for (auto& frame : got_stack) { + ss << " " << FrameString(frame) << "\n"; + } + OnError(ss.str()); + } + } + void ExpectLocal(const std::string& name, int64_t value) override { DEBUGGER_LOG("ExpectLocal('%s', %d)", name.c_str(), (int)value); ExpectLocalT(name, value); @@ -626,7 +684,7 @@ class Thread : public debug::Thread { template void ExpectLocalT(const std::string& name, const T& expect) { dap::StackFrame frame; - if (!client_.TopStackFrame(threadId_, &frame)) { + if (!client_.TopStackFrame(thread_id_, &frame)) { return; } @@ -677,7 +735,28 @@ class Thread : public debug::Thread { error_ += err; } - const dap::integer threadId_; + std::string FrameString(const dap::StackFrame& frame) { + std::stringstream ss; + ss << frame.name; + if (frame.source.has_value() && frame.source->name.has_value()) { + ss << " " << frame.source->name.value() << ":" << frame.line; + } + return ss.str(); + } + + std::string FrameString(const debug::StackFrame& frame) { + std::stringstream ss; + ss << frame.name; + if (frame.location.file != "") { + ss << " " << frame.location.file; + if (frame.location.line != 0) { + ss << ":" << frame.location.line; + } + } + return ss.str(); + } + + const dap::integer thread_id_; const int lane_; Client client_; std::thread thread_; diff --git a/tests/cases/debugger_hlsl_line_stepping.amber b/tests/cases/debugger_hlsl_line_stepping.amber index c7e04c902..011c50bb0 100644 --- a/tests/cases/debugger_hlsl_line_stepping.amber +++ b/tests/cases/debugger_hlsl_line_stepping.amber @@ -328,6 +328,10 @@ CLEAR pipeline DEBUG pipeline DRAW_ARRAY AS TRIANGLE_LIST START_IDX 0 COUNT 6 THREAD VERTEX_INDEX 2 + EXPECT CALLSTACK + "main" "simple_vs.hlsl" 9 + "VertexShader" + END EXPECT LOCATION "simple_vs.hlsl" 9 " VS_OUTPUT vout;" EXPECT LOCAL "pos.x" EQ -1.007874 EXPECT LOCAL "pos.y" EQ 1.000000 diff --git a/tests/cases/debugger_spirv_line_stepping.amber b/tests/cases/debugger_spirv_line_stepping.amber index d7b5a67b3..b672d6f05 100644 --- a/tests/cases/debugger_spirv_line_stepping.amber +++ b/tests/cases/debugger_spirv_line_stepping.amber @@ -89,6 +89,9 @@ END # there are no race conditions. DEBUG pipeline 1 1 1 THREAD GLOBAL_INVOCATION_ID 2 0 0 + EXPECT CALLSTACK + "ComputeShader" "ComputeShader0.spvasm" 20 + END EXPECT LOCATION "ComputeShader0.spvasm" 20 "%5 = OpVariable %11 Uniform" STEP_IN EXPECT LOCATION "ComputeShader0.spvasm" 25 "%2 = OpVariable %15 Input" @@ -109,6 +112,9 @@ DEBUG pipeline 1 1 1 STEP_IN EXPECT LOCATION "ComputeShader0.spvasm" 36 "OpStore %23 %22" STEP_IN + EXPECT CALLSTACK + "ComputeShader" "ComputeShader0.spvasm" 37 + END EXPECT LOCATION "ComputeShader0.spvasm" 37 "OpReturn" CONTINUE END