diff --git a/clients/vscode-hlasmplugin/CHANGELOG.md b/clients/vscode-hlasmplugin/CHANGELOG.md index a63412d74..4953a72cf 100644 --- a/clients/vscode-hlasmplugin/CHANGELOG.md +++ b/clients/vscode-hlasmplugin/CHANGELOG.md @@ -8,6 +8,7 @@ - Watch support in the Macro tracer - Emit MNOTE and PUNCH arguments to the debug console - Make MNOTE and PUNCH outputs available from VSCode +- Function breakpoint support in the Macro tracer #### Fixed - Unknown requests were dropped without a proper response diff --git a/clients/vscode-hlasmplugin/src/test/suite/debugging.test.ts b/clients/vscode-hlasmplugin/src/test/suite/debugging.test.ts index 07de3cae9..476743694 100644 --- a/clients/vscode-hlasmplugin/src/test/suite/debugging.test.ts +++ b/clients/vscode-hlasmplugin/src/test/suite/debugging.test.ts @@ -109,4 +109,23 @@ suite('Debugging Test Suite', () => { await helper.debugStop(); }).timeout(20000).slow(10000); + + test('Function breakpoint test', async () => { + await helper.addFunctionBreakpoints(['uniQue_maCro']); + + await helper.showDocument('function_break'); + + const session = await helper.debugStartSession(); + + // Continue until breakpoint is hit + await helper.debugContinue(); + const a0 = await session.customRequest('evaluate', { expression: "&A" }); + assert.deepStrictEqual(a0, { result: '0', variablesReference: 0 }); + + await helper.debugContinue(); + const a1 = await session.customRequest('evaluate', { expression: "&A" }); + assert.deepStrictEqual(a1, { result: '1', variablesReference: 0 }); + + await helper.debugStop(); + }).timeout(20000).slow(10000); }); diff --git a/clients/vscode-hlasmplugin/src/test/suite/testHelper.ts b/clients/vscode-hlasmplugin/src/test/suite/testHelper.ts index 2a280d8bf..9c09f2562 100644 --- a/clients/vscode-hlasmplugin/src/test/suite/testHelper.ts +++ b/clients/vscode-hlasmplugin/src/test/suite/testHelper.ts @@ -81,6 +81,10 @@ export async function addBreakpoints(file: string, lines: Array) { await vscode.debug.addBreakpoints(lines.map(l => new vscode.SourceBreakpoint(new vscode.Location(document.uri, new vscode.Position(l, 0)), true))); } +export async function addFunctionBreakpoints(functions: Array) { + await vscode.debug.addBreakpoints(functions.map(f => new vscode.FunctionBreakpoint(f))); +} + export async function removeAllBreakpoints() { await vscode.debug.removeBreakpoints(vscode.debug.breakpoints); } diff --git a/clients/vscode-hlasmplugin/src/test/workspace/function_break b/clients/vscode-hlasmplugin/src/test/workspace/function_break new file mode 100644 index 000000000..f8b63545f --- /dev/null +++ b/clients/vscode-hlasmplugin/src/test/workspace/function_break @@ -0,0 +1,8 @@ + MACRO + UNIQUE_MACRO + MEND + +&A SETA 0 + UNIQUE_MACRO +&A SETA 1 + UNIQUE_MACRO diff --git a/language_server/src/dap/dap_feature.cpp b/language_server/src/dap/dap_feature.cpp index 48fd94b19..3ca280d1d 100644 --- a/language_server/src/dap/dap_feature.cpp +++ b/language_server/src/dap/dap_feature.cpp @@ -89,6 +89,7 @@ void dap_feature::register_methods(std::map& methods) add_method("launch", &dap_feature::on_launch, LOG_EVENT); add_method("setBreakpoints", &dap_feature::on_set_breakpoints, LOG_EVENT); add_method("setExceptionBreakpoints", &dap_feature::on_set_exception_breakpoints, LOG_EVENT); + add_method("setFunctionBreakpoints", &dap_feature::on_set_function_breakpoints, LOG_EVENT); add_method("configurationDone", &dap_feature::on_configuration_done); add_method("threads", &dap_feature::on_threads); add_method("stackTrace", &dap_feature::on_stack_trace); @@ -145,6 +146,7 @@ void dap_feature::on_initialize(const request_id& requested_seq, const nlohmann: nlohmann::json { { "supportsConfigurationDoneRequest", true }, { "supportsEvaluateForHovers", true }, + { "supportsFunctionBreakpoints", true }, }); line_1_based_ = args.at("linesStartAt1").get() ? 1 : 0; @@ -231,6 +233,29 @@ void dap_feature::on_set_exception_breakpoints(const request_id& request_seq, co response_->respond(request_seq, "setExceptionBreakpoints", nlohmann::json()); } +void dap_feature::on_set_function_breakpoints(const request_id& request_seq, const nlohmann::json& args) +{ + if (!debugger) + return; + + nlohmann::json breakpoints_verified = nlohmann::json::array(); + std::vector breakpoints; + + if (auto bpoints_found = args.find("breakpoints"); bpoints_found != args.end()) + { + for (auto& bp_json : bpoints_found.value()) + { + breakpoints.emplace_back(parser_library::sequence(bp_json.at("name").get())); + breakpoints_verified.push_back(nlohmann::json { { "verified", true } }); + } + } + + debugger->function_breakpoints(breakpoints); + + response_->respond( + request_seq, "setFunctionBreakpoints", nlohmann::json { { "breakpoints", breakpoints_verified } }); +} + void dap_feature::on_configuration_done(const request_id& request_seq, const nlohmann::json&) { response_->respond(request_seq, "configurationDone", nlohmann::json()); diff --git a/language_server/src/dap/dap_feature.h b/language_server/src/dap/dap_feature.h index fb79f1170..0200d376b 100644 --- a/language_server/src/dap/dap_feature.h +++ b/language_server/src/dap/dap_feature.h @@ -54,6 +54,7 @@ class dap_feature : public feature, public hlasm_plugin::parser_library::debuggi void on_launch(const request_id& request_seq, const nlohmann::json& args); void on_set_breakpoints(const request_id& request_seq, const nlohmann::json& args); void on_set_exception_breakpoints(const request_id& request_seq, const nlohmann::json& args); + void on_set_function_breakpoints(const request_id& request_seq, const nlohmann::json& args); void on_configuration_done(const request_id& request_seq, const nlohmann::json& args); void on_threads(const request_id& request_seq, const nlohmann::json& args); void on_stack_trace(const request_id& request_seq, const nlohmann::json& args); diff --git a/language_server/test/dap/dap_feature_test.cpp b/language_server/test/dap/dap_feature_test.cpp index c22654dd5..d8139ab96 100644 --- a/language_server/test/dap/dap_feature_test.cpp +++ b/language_server/test/dap/dap_feature_test.cpp @@ -351,6 +351,41 @@ TEST_F(feature_launch_test, breakpoint) feature.on_disconnect(request_id(5), {}); } +const std::string file_function_breakpoint = R"( + LR 1,1 + SAM31 +)"; + +TEST_F(feature_launch_test, function_breakpoint) +{ + ws_mngr->did_open_file(utils::path::path_to_uri(file_path).c_str(), + 0, + file_function_breakpoint.c_str(), + file_function_breakpoint.size()); + ws_mngr->idle_handler(); + + nlohmann::json bp_args { { "breakpoints", R"([{"name":"sam31"}])"_json } }; + feature.on_set_function_breakpoints(request_id(47), bp_args); + std::vector expected_resp_bp = { + { request_id(47), "setFunctionBreakpoints", R"( { "breakpoints":[ {"verified":true} ]})"_json } + }; + EXPECT_EQ(resp_provider.responses, expected_resp_bp); + resp_provider.reset(); + + feature.on_launch(request_id(0), nlohmann::json { { "program", file_path }, { "stopOnEntry", false } }); + ws_mngr->idle_handler(); + feature.idle_handler(nullptr); + std::vector expected_resp = { { request_id(0), "launch", nlohmann::json() } }; + EXPECT_EQ(resp_provider.responses, expected_resp); + wait_for_stopped(); + resp_provider.reset(); + + check_simple_stack_trace(request_id(2), 2); + + + feature.on_disconnect(request_id(3), {}); +} + const std::string file_variables = R"(&VARA SETA 4 &VARB SETB 1 &VARC SETC 'STH' diff --git a/language_server/test/dap/dap_server_test.cpp b/language_server/test/dap/dap_server_test.cpp index 1e8a65e05..0c737f713 100644 --- a/language_server/test/dap/dap_server_test.cpp +++ b/language_server/test/dap/dap_server_test.cpp @@ -57,7 +57,7 @@ TEST(dap_server, dap_server) serv.message_received(initialize_message); std::vector expected_response_init = { - R"({"body":{"supportsConfigurationDoneRequest":true,"supportsEvaluateForHovers":true},"command":"initialize","request_seq":1,"seq":1,"success":true,"type":"response"})"_json, + R"({"body":{"supportsConfigurationDoneRequest":true,"supportsEvaluateForHovers":true,"supportsFunctionBreakpoints":true},"command":"initialize","request_seq":1,"seq":1,"success":true,"type":"response"})"_json, R"({"body":null,"event" : "initialized","seq" : 2,"type" : "event"})"_json }; diff --git a/parser_library/include/debugger.h b/parser_library/include/debugger.h index 71fcad05b..dfce571b8 100644 --- a/parser_library/include/debugger.h +++ b/parser_library/include/debugger.h @@ -15,6 +15,8 @@ #ifndef HLASMPLUGIN_PARSERLIBRARY_DEBUGGER_H #define HLASMPLUGIN_PARSERLIBRARY_DEBUGGER_H +#include + #include "parser_library_export.h" #include "protocol.h" #include "range.h" @@ -120,6 +122,12 @@ class debugger [[nodiscard]] breakpoints_t breakpoints(sequence source) const; [[nodiscard]] breakpoints_t breakpoints(std::string_view source) const { return breakpoints(sequence(source)); } + void function_breakpoints(sequence bps); + void function_breakpoints(std::span bps) + { + function_breakpoints(sequence(bps)); + } + // Retrieval of current context. stack_frames_t stack_frames() const; scopes_t scopes(frame_id_t frame_id) const; diff --git a/parser_library/include/protocol.h b/parser_library/include/protocol.h index 74fc4daca..4fecf4081 100644 --- a/parser_library/include/protocol.h +++ b/parser_library/include/protocol.h @@ -401,12 +401,20 @@ using variables_t = sequence; struct breakpoint { - breakpoint(size_t line) + explicit breakpoint(size_t line) : line(line) {} size_t line; }; +struct function_breakpoint +{ + explicit function_breakpoint(sequence name) + : name(name) + {} + sequence name; +}; + struct output_line { int level; // -1 if N/A diff --git a/parser_library/src/debugging/debugger.cpp b/parser_library/src/debugging/debugger.cpp index e94871b2b..c09d32fbc 100644 --- a/parser_library/src/debugging/debugger.cpp +++ b/parser_library/src/debugging/debugger.cpp @@ -175,6 +175,8 @@ class debugger::impl final : public processing::statement_analyzer, output_handl utils::resource::resource_location_hasher> breakpoints_; + std::unordered_set> function_breakpoints_; + size_t add_variable(std::vector vars) { variables_[next_var_ref_].variables = std::move(vars); @@ -307,10 +309,13 @@ class debugger::impl final : public processing::statement_analyzer, output_handl if (!resolved_stmt) return false; + const auto& op_code = resolved_stmt->opcode_ref().value; // Continue only for non-empty statements - if (resolved_stmt->opcode_ref().value.empty()) + if (op_code.empty()) return false; + const bool function_breakpoint_hit = function_breakpoints_.contains(op_code.to_string_view()); + range stmt_range = resolved_stmt->stmt_range_ref(); bool breakpoint_hit = false; @@ -333,7 +338,8 @@ class debugger::impl final : public processing::statement_analyzer, output_handl }; // breakpoint check - if (stop_on_next_stmt_ || breakpoint_hit || (stop_on_stack_changes_ && stack_condition_violated(stack_node))) + if (stop_on_next_stmt_ || breakpoint_hit || function_breakpoint_hit + || (stop_on_stack_changes_ && stack_condition_violated(stack_node))) { variables_.clear(); stack_frames_.clear(); @@ -348,8 +354,17 @@ class debugger::impl final : public processing::statement_analyzer, output_handl stop_on_stack_condition_ = std::make_pair(stack_node, nullptr); continue_ = false; + + static constexpr const std::string_view reasons[] = { + "entry", + "breakpoint", + "function breakpoint", + "breakpoint", + }; + const auto reason_id = breakpoint_hit + 2 * function_breakpoint_hit; + if (event_) - event_->stopped("entry", ""); + event_->stopped(reasons[reason_id], ""); } return !continue_; } @@ -812,6 +827,13 @@ class debugger::impl final : public processing::statement_analyzer, output_handl return {}; } + void function_breakpoints(std::span bps) + { + function_breakpoints_.clear(); + for (const auto& bp : bps) + function_breakpoints_.emplace(utils::to_upper_copy(std::string_view(bp.name))); + } + ~impl() { disconnect(); } }; @@ -866,6 +888,11 @@ breakpoints_t debugger::breakpoints(sequence source) const return result; } +void debugger::function_breakpoints(sequence bps) +{ + pimpl->function_breakpoints(std::span(bps.begin(), bps.end())); +} + stack_frames_t debugger::stack_frames() const { const auto& frames = pimpl->stack_frames(); diff --git a/parser_library/test/debugging/debug_event_consumer_s_mock.cpp b/parser_library/test/debugging/debug_event_consumer_s_mock.cpp index e6eff00c6..af2e30c96 100644 --- a/parser_library/test/debugging/debug_event_consumer_s_mock.cpp +++ b/parser_library/test/debugging/debug_event_consumer_s_mock.cpp @@ -23,7 +23,7 @@ using namespace std::chrono_literals; void debug_event_consumer_s_mock::stopped( sequence reason, hlasm_plugin::parser_library::sequence addtl_info) { - (void)reason; + last_reason = std::string_view(reason); (void)addtl_info; ++stop_count; stopped_ = true; diff --git a/parser_library/test/debugging/debug_event_consumer_s_mock.h b/parser_library/test/debugging/debug_event_consumer_s_mock.h index 2fcf76bbb..d847ebc01 100644 --- a/parser_library/test/debugging/debug_event_consumer_s_mock.h +++ b/parser_library/test/debugging/debug_event_consumer_s_mock.h @@ -19,6 +19,7 @@ class debug_event_consumer_s_mock : public hlasm_plugin::parser_library::debugging::debug_event_consumer { + std::string last_reason; bool stopped_ = false; bool exited_ = false; size_t stop_count = 0; @@ -54,6 +55,8 @@ class debug_event_consumer_s_mock : public hlasm_plugin::parser_library::debuggi const auto& get_last_mnote() const { return last_mnote; } const auto& get_last_punch() const { return last_punch; } + + const auto& get_last_reason() const { return last_reason; } }; #endif // !HLASMPLUGIN_PARSERLIBRARY_TEST_DEBUG_EVENT_CONSUMER_S_MOCK_H diff --git a/parser_library/test/debugging/debugger_test.cpp b/parser_library/test/debugging/debugger_test.cpp index 97888ae92..9063437a4 100644 --- a/parser_library/test/debugging/debugger_test.cpp +++ b/parser_library/test/debugging/debugger_test.cpp @@ -1252,6 +1252,39 @@ TEST(debugger, breakpoints_set_get) EXPECT_EQ(bp.line, bps.begin()->line); } +TEST(debugger, function_breakpoints) +{ + std::string open_code = R"( + LR 1,1 + SAM31 +)"; + + file_manager_impl file_manager; + NiceMock dc_provider; + EXPECT_CALL(dc_provider, provide_debugger_configuration).WillRepeatedly(Invoke([&file_manager](auto, auto r) { + r.provide({ .fm = &file_manager }); + })); + debugger d; + debug_event_consumer_s_mock m(d); + + const resource_location file_loc("test"); + + file_manager.did_open_file(file_loc, 0, open_code); + + function_breakpoint bp(sequence(std::string_view("SAM31"))); + d.function_breakpoints(sequence(&bp, 1)); + + auto [resp, mock] = make_workspace_manager_response(std::in_place_type>); + EXPECT_CALL(*mock, provide(true)); + d.launch(file_loc.get_uri(), dc_provider, false, resp); + + m.wait_for_stopped(); + + EXPECT_EQ(m.get_last_reason(), "function breakpoint"); + + d.disconnect(); +} + TEST(debugger, invalid_file) { file_manager_impl file_manager;