From 96569493c8a2e67b294cd9142b84366fa8fb7898 Mon Sep 17 00:00:00 2001 From: kr-2003 Date: Tue, 21 Oct 2025 11:39:34 +0530 Subject: [PATCH 1/2] xeus-cpp debugger with tests --- .gitignore | 2 + CMakeLists.txt | 8 + include/xeus-cpp/xdebugger.hpp | 118 +++ include/xeus-cpp/xinterpreter.hpp | 25 + .../kernels/xcpp17-debugger/kernel.json.in | 21 + .../kernels/xcpp17-debugger/logo-32x32.png | Bin 0 -> 1520 bytes .../kernels/xcpp17-debugger/logo-64x64.png | Bin 0 -> 3113 bytes .../kernels/xcpp17-debugger/logo-svg.svg | 25 + .../xcpp17-debugger/wasm_kernel.json.in | 17 + src/main.cpp | 21 +- src/xdebugger.cpp | 723 ++++++++++++++++++ src/xdebuglldb_client.cpp | 65 ++ src/xdebuglldb_client.hpp | 40 + src/xinternal_utils.cpp | 34 + src/xinternal_utils.hpp | 23 + src/xinterpreter.cpp | 18 +- test/CMakeLists.txt | 5 +- test/debugger/__init__.py | 1 + test/debugger/temp.json | 16 + test/debugger/test_debugger.py | 336 ++++++++ test/debugger/util.py | 332 ++++++++ 21 files changed, 1823 insertions(+), 7 deletions(-) create mode 100644 include/xeus-cpp/xdebugger.hpp create mode 100644 share/jupyter/kernels/xcpp17-debugger/kernel.json.in create mode 100644 share/jupyter/kernels/xcpp17-debugger/logo-32x32.png create mode 100644 share/jupyter/kernels/xcpp17-debugger/logo-64x64.png create mode 100644 share/jupyter/kernels/xcpp17-debugger/logo-svg.svg create mode 100644 share/jupyter/kernels/xcpp17-debugger/wasm_kernel.json.in create mode 100644 src/xdebugger.cpp create mode 100644 src/xdebuglldb_client.cpp create mode 100644 src/xdebuglldb_client.hpp create mode 100644 src/xinternal_utils.cpp create mode 100644 src/xinternal_utils.hpp create mode 100644 test/debugger/__init__.py create mode 100644 test/debugger/temp.json create mode 100644 test/debugger/test_debugger.py create mode 100644 test/debugger/util.py diff --git a/.gitignore b/.gitignore index a706da96..d6fa6194 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ bld # MacOS specific files *.DS_Store + +.cache/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e1648fd..8187ba70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -164,6 +164,10 @@ configure_kernel("/share/jupyter/kernels/xc11/") configure_kernel("/share/jupyter/kernels/xc17/") configure_kernel("/share/jupyter/kernels/xc23/") +if(NOT EMSCRIPTEN AND NOT WIN32) + configure_kernel("/share/jupyter/kernels/xcpp17-debugger/") +endif() + # Source files # ============ @@ -178,6 +182,7 @@ set(XEUS_CPP_HEADERS include/xeus-cpp/xmagics.hpp include/xeus-cpp/xoptions.hpp include/xeus-cpp/xpreamble.hpp + include/xeus-cpp/xdebugger.hpp #src/xinspect.hpp #src/xsystem.hpp #src/xparser.hpp @@ -192,6 +197,9 @@ set(XEUS_CPP_SRC src/xparser.cpp src/xutils.cpp src/xmagics/os.cpp + src/xdebugger.cpp + src/xdebuglldb_client.cpp + src/xinternal_utils.cpp ) if(NOT EMSCRIPTEN) diff --git a/include/xeus-cpp/xdebugger.hpp b/include/xeus-cpp/xdebugger.hpp new file mode 100644 index 00000000..46979fae --- /dev/null +++ b/include/xeus-cpp/xdebugger.hpp @@ -0,0 +1,118 @@ +/************************************************************************************ + * Copyright (c) 2023, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ + +#ifndef XEUS_CPP_XDEBUGGER_HPP +#define XEUS_CPP_XDEBUGGER_HPP + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wattributes" +#endif + +#include + +#include "nlohmann/json.hpp" +#include "xeus-cpp/xinterpreter.hpp" +#include "xeus-zmq/xdebugger_base.hpp" +#include "xeus_cpp_config.hpp" + +namespace xcpp +{ + class xdebuglldb_client; + + class XEUS_CPP_API debugger : public xeus::xdebugger_base + { + public: + + using base_type = xeus::xdebugger_base; + + debugger( + xeus::xcontext& context, + const xeus::xconfiguration& config, + const std::string& user_name, + const std::string& session_id, + const nl::json& debugger_config + ); + + virtual ~debugger(); + + private: + + nl::json inspect_variables_request(const nl::json& message); + nl::json stack_trace_request(const nl::json& message); + nl::json attach_request(const nl::json& message); + nl::json configuration_done_request(const nl::json& message); + nl::json set_breakpoints_request(const nl::json& message); + nl::json dump_cell_request(const nl::json& message); + nl::json rich_inspect_variables_request(const nl::json& message); + nl::json copy_to_globals_request(const nl::json& message); + nl::json source_request(const nl::json& message); + + bool get_variable_info_from_lldb( + const std::string& var_name, + int frame_id, + nl::json& data, + nl::json& metadata, + int sequence + ); + void get_container_info( + const std::string& var_name, + int frame_id, + const std::string& var_type, + nl::json& data, + nl::json& metadata, + int sequence, + int size + ); + bool is_container_type(const std::string& type) const; + + bool start_lldb(); + bool start() override; + void stop() override; + xeus::xdebugger_info get_debugger_info() const override; + virtual std::string get_cell_temporary_file(const std::string& code) const override; + + bool connect_to_lldb_tcp(); + std::string send_dap_message(const nl::json& message); + std::string receive_dap_response(); + + xdebuglldb_client* p_debuglldb_client; + std::string m_lldb_host; + std::string m_lldb_port; + nl::json m_debugger_config; + bool m_is_running; + int m_tcp_socket; + bool m_tcp_connected; + pid_t jit_process_pid; + + using breakpoint_list_t = std::map>; + breakpoint_list_t m_breakpoint_list; + + xcpp::interpreter* m_interpreter; + + std::unordered_map> m_hash_to_sequential; + std::unordered_map m_sequential_to_hash; + + bool m_copy_to_globals_available; + }; + + XEUS_CPP_API + std::unique_ptr make_cpp_debugger( + xeus::xcontext& context, + const xeus::xconfiguration& config, + const std::string& user_name, + const std::string& session_id, + const nl::json& debugger_config + ); +} + +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +#endif \ No newline at end of file diff --git a/include/xeus-cpp/xinterpreter.hpp b/include/xeus-cpp/xinterpreter.hpp index 25eeb049..f35c0f4e 100644 --- a/include/xeus-cpp/xinterpreter.hpp +++ b/include/xeus-cpp/xinterpreter.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "clang/Interpreter/CppInterOp.h" // from CppInterOp package @@ -39,6 +40,27 @@ namespace xcpp void publish_stdout(const std::string&); void publish_stderr(const std::string&); + static int32_t get_current_pid(); + + std::vector get_execution_count(const std::string& code) const + { + auto it = m_code_to_execution_count_map.find(code); + if (it != m_code_to_execution_count_map.end()) + { + return it->second; + } + return {}; + } + + std::string get_code_from_execution_count(int execution_count) const + { + auto it = m_execution_count_to_code_map.find(execution_count); + if(it != m_execution_count_to_code_map.end()) + { + return it->second; + } + return ""; + } private: @@ -85,6 +107,9 @@ namespace xcpp xoutput_buffer m_cout_buffer; xoutput_buffer m_cerr_buffer; + + std::map> m_code_to_execution_count_map; + std::map m_execution_count_to_code_map; }; } diff --git a/share/jupyter/kernels/xcpp17-debugger/kernel.json.in b/share/jupyter/kernels/xcpp17-debugger/kernel.json.in new file mode 100644 index 00000000..d9a27220 --- /dev/null +++ b/share/jupyter/kernels/xcpp17-debugger/kernel.json.in @@ -0,0 +1,21 @@ +{ + "display_name": "C++17-Debugger", + "env": { + "PATH":"@XEUS_CPP_PATH@", + "LD_LIBRARY_PATH":"@XEUS_CPP_LD_LIBRARY_PATH@" + }, + "argv": [ + "@XEUS_CPP_KERNELSPEC_PATH@xcpp", + "-f", + "{connection_file}", + "-g", + "-O0", + "--use-oop-jit", + "-resource-dir", "@XEUS_CPP_RESOURCE_DIR@", + "-I", "@XEUS_CPP_INCLUDE_DIR@", + "-std=c++17" + ], + "language": "cpp", + "metadata": {"debugger": true + } +} \ No newline at end of file diff --git a/share/jupyter/kernels/xcpp17-debugger/logo-32x32.png b/share/jupyter/kernels/xcpp17-debugger/logo-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..c09c458544bb83b9e3ff4d38f5758bda10ceebe8 GIT binary patch literal 1520 zcmV`fj+1!di&IS(aK;ELYw|L~OnvC0l61ks3o346fp`H$NMM0omj!m; z_kH@H6w9)^K>k0T=Y7uqf1dN4=RHR-3_SCt%^Kb<)&MU7;Zlg5y2-oY^X2I+L-i$w zx?#n~wk$=V07O#1r4C?Ac(>)lg4HQKH(Wq=p)FksC6OXrNjh{uNT#gE{aMoakX3XTk{(FnV(Oe z@2s~w9%t9`^9n$i$CVI>+-wrO`tbNclf$)>dTY@HMq2(mOewM3E;b(gqU`v@`quU| z&?f^Z0J=#DSPf5QBrs{LbN<3m*yuT1l|^h>)_7v6XNK(hD<51m$Gp!i2?Hs zFyZD1GE)5`@AeoRtZC!y-<<#?jkYj5HI~Q-Uw67;ILoSQ?bVI#nSnTh3V0+f&aX}z z+PcZ#S4T&u_g`COos*JNt-LrdktqoyXmNVH?|fTz?!E6TCllfg2`C^uOkv8n5k5AL zX0Ua?FL^KAJv!TdtaGQ2w_aYgqb#wjN1GgsIHZ82Q5MW5-}n!o?V!!&PYxJuo$f<> zZ8eh$O1vq(#>ehTqQ`6S!^ukkJbCX#nw?#gRyBqe5M@>UY!{l{eztCpX4`+NHoVz+ zHKVWnvAdF3yXa0lngKwhMP*IaRN7oU2;jh}#^3@%K}wpARsDJXWX;)VB4aevn}+2l zpa91Jd7cm#!S6Ng?hBV1Dt@V`yj#=sYf*RD4b1BAM2(t+qKpCfrlKCeCj~+XzZ58e z1k|^7q3OO4>OINP#*q^h&I5;z&CxX7(q~CQ^Jc{Kv|Ha^kXjq2Dm}d-ddUHHm!I+vmF=K3XAm4o<09I^2 zovavoZU4pOcuU)x3zK8}|9GytJ#61w$!}E`0hlpqEXyC7PJFCy*_NHU=vA?5WbU2gZGAIi!T98TAL0IwA6BY9jbbEn@Hip&RR8tgp2sTe~_ zGnzLO`;~SgtmRlEX96Y^DdGAZ@Pv)Htm)c`((;OIbtZ_{%Kv7I2LIcHiD8=sv+=F~(6 z`hlLPZQ{M+!|eH`3K_H`VKb$<8+-)-%*{*1Bty1janV+0-F^$Vj)^2pHPK{uakS<# z=Nj9CZ3C!8QF17GyR6qRa33%~Z$8L;U`i;DgM=L+@`z5|N^y{;)CV&PRbrc0A?5#S z%MolthUIc#tz)1MLQh8bZyb%PtVc5R!9*vApL2&dnE2s=jp-=jBM>1*GtMBz84XAq=sq3n3 z^=Q_!)g~c!+q~quu9-OAo?0!V)>cdGqFI(tTeg~MXdzxOyl`DEa#8LB3@`(8pL4$b z13|_am@{Vvvfp1b=X<~Rd(ZED=l$OIJKsz2D`4pd1@kDuE0oYlghEOvN&xIPd;9PD zBq_!?n6{9?9PGKhd1FK@*#Tf~A0oAs&~@ocChP|xbTDih6arrScYcsnj5PpO0`TV1 zC6nQpH(i`_YZ9-xF-ot_e>8VLJ<0VH7Q2Zb{z z;nDz1a8p$~fH!@NzG?qjHp=7y(`?{z0+K!|h!k77tpIt(!>Y>wvTD!LxFeoa>M;VI z*_S8tHDEO*WDNixPw91l41im)XX!mfo>1mK0hE%Z9~LYG@HQogbl)UiuuwvGS;gFj zPm-BIKg0Vy9ACIDO-fJArnxPexHZfXxK-FaxyB36h3K?FRv_j0({D&9Z|mH=Rc z=cMkpTejD3TKdP=Y{qAo67sU%!q zLVXhO^3LKRCW$$fQ1UVWk-OH&rDEI_?uR?WWC#nCAjr=PGVd<6Ix~w-qZRdSCR8^Y zP*UHChIWs|JULzdA?}=`Mu!BXd{{6ap!)#?x-&8%z`ZeYJP;d<*l<4(7d%dFs}bj~ zw&8SfD@>L?A0S_F0JaaWk9+Ob*d;7yUkW$-y-R9k1vL`QwfNU3Pq#{Uyzt10mk)bl7D*A+6Dn{byV5l@E zG_>*e50pZ6@mg)(@%*xQt-+)KNHq$+yU^V(?k!9x87b7j=wZHCF((ExAGiHwpYRe9 zEd6~HzVxlfsUo#AabvYwlYOeFJi)@UQ+lp)xU;vA4FL>C#T?^$a2OUS!SXqG4Cu{w zgAm|}#Bg+)So~7%I1SJlO*u#NO2agI4iYd5F|Yq0WUEFP_z}*HJ<)B z>tb|8lX{{{dfYK$ltPBY(LpZv(-~Qmt8{49Szxwuko$TeYN!vwgIwE280znhQ6Z9| z*AAUk7|d2dk*Zq+EJz4*DVwTn(&M{JDwNdez)|}(Jyb5itO-LgbxaT#Cs9*LeWx}h z{dD`8l7?|Enr#EY1dLY5Fd|gSOP1p(zRGUEPep3<7s{wKCVZAvhqI+^cz$LSf@NNI zGR<1EW!rxXE8aSKB^E${FzatZun(9T8_Y|NqZGR{s&UH6&G$xClOAs#u7JA3+;h!n z;pi7d!k* ziLW(l1;;6UK@b5WLZpz0oll;|b`wq%vBaROlk@O<}= zpsg-%! zPd$PN2z20`Z$+cV1igv(MlIDCjoHU?OQSjrrs@3&GK9d-M+`ron>P60_}yW&K*TVR zdW#^Fbln;uK!9&oyVhW_(p@J68H#UL&FpClmHYOzi=O((F8&y>0G>DrktnDtujFdk#iEiS)Gr9| z9{g|`5J&)@Eh@RJPt}(C=9cq6W?hIkTP@Q702(zQ66HWXBpf2~@c#5(s%n8ec^|?8 zr6_%SVb@s6`v7#k0*x92S0 zZ~6h9-jMVC>2sm&+7A1;on;fy+N6iKBohI_lR${WT@{W}Xbl$l`P{Uj!)WPof>xG; z*1$n$vH}1ob^PvkLl6OtJa%juLNFmJ0O!lvz;ayW>72{0<=1PSrKwVwP1B)nY=G21 z7gE1O^s#ZaPL7ZUq238cUVI4q$??nQ&Roo& z%uzam_W+bCpwlX#?aR4w%_({2+9Sjn)JN_ z(>O{65q)<|$heA~PmlTPr75bg0EyE*Fd+~`fR&}Va*bb*vn6Q;9-HEN?h-L90Eag{ z0bhw2k57#aE#2{W@YYAiRd|URkDd|{oDF7`w(=W*5FAQfh?NWOVxy zp*OeI1Edo2+M@eM7XR~6=E;eQ5+OD_v`=Wqs|`AotN8f>5ks)!h1tk{FBx+ZMnWud zJnY!$VMu>tHK0IeMcoGYqDb(02qlLd-kKTI60Ky2htm-JYy9FpEST6Ex2qXG=W zZ-atj7Y;6Ysr#=W=VY<4ML(X)rgf_3IE8FJlK&X*QvYCoU(COMl#roqP}$Ii|6~@9 zgNbc}RC*dRc_LiACzpgdN*gZ~7FXn!l=Y;kD|Vg0l<|=+KRtJ0vaon|+hG{G0-hk~ zbq_#qs6>Fg6%yY`eD(140fw4}#>{=+9QDsFE}POjNAEP4@kr`dXx8xa2c7_>z~b$P zaiOH{mKJUVv~82ns>%Utso|r`h5(Ks)?3Hw=Hits1J`YNDx3>2FV?zKXw4(dBg|(Lakaa%);P)BR zI&_^8&J4SJwE@#seu|3vHeNR(1O}58i#C6aoriPyhAkUOo>`&TwnH-VT?qHem7g?@&AHy4~HHcmmX z0Y9d0vc2R8GdIoy$Y*f#hzvsr9-A7Al)3SkdG82_T=?>QrMwZJp2$P`u`AFS`Ry+C z0}jA8Oy$-=sqFuVOb`B`3aUty%+G zw0aa)HR00L2AsTd4P~_+%YAY{u^cB;&pT>#{zzsi?*V1f0Q9?Ac^jf0z*=Oi-w*U% zeZqfE&ffSixZlJE{COY1q`zSCtg48YDf3#1s>Xz0EVW4l3(k%yc&dTM&Ov8I5ddI_fLW^b2))2{{wp=5@|3jCCMo0QDlirHHt z!O^V%^?cD3C9809{q0#aWrv40&H!Q30P23(93gK)EKkF+Ww(FXoDGyp24|S30SGLd zBnQB-f5Qo-zzoPa2B&s--Wx$Mnb!b>Z|UsM5Mc$5r~bA}XYDX!^B9O|-^J4ewV+t% zEPVAO2A6 + + + + + + + + + + + + + + + + diff --git a/share/jupyter/kernels/xcpp17-debugger/wasm_kernel.json.in b/share/jupyter/kernels/xcpp17-debugger/wasm_kernel.json.in new file mode 100644 index 00000000..fc2a3d61 --- /dev/null +++ b/share/jupyter/kernels/xcpp17-debugger/wasm_kernel.json.in @@ -0,0 +1,17 @@ +{ + "display_name": "C++17", + "argv": [ + "@XEUS_CPP_KERNELSPEC_PATH@xcpp", + "-resource-dir", "/lib/clang/@CPPINTEROP_LLVM_VERSION_MAJOR@", + "-std=c++17", "-mllvm", "-enable-emscripten-cxx-exceptions", + "-mllvm", "-enable-emscripten-sjlj", "-msimd128" + ], + "language": "cpp", + "metadata": { + "debugger": false, + "shared": { + "libxeus.so": "lib/libxeus.so", + "libclangCppInterOp.so": "lib/libclangCppInterOp.so" + } + } +} diff --git a/src/main.cpp b/src/main.cpp index 00d52d28..569d2e8e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,16 +19,19 @@ #include #endif +#include "nlohmann/json.hpp" #include "xeus/xhelper.hpp" #include #include #include "xeus-zmq/xzmq_context.hpp" #include +#include "xeus-zmq/xserver_zmq_split.hpp" #include "xeus-cpp/xeus_cpp_config.hpp" #include "xeus-cpp/xinterpreter.hpp" #include "xeus-cpp/xutils.hpp" +#include "xeus-cpp/xdebugger.hpp" int main(int argc, char* argv[]) { @@ -62,6 +65,12 @@ int main(int argc, char* argv[]) auto interpreter = std::make_unique(argc, argv); std::unique_ptr context = xeus::make_zmq_context(); + nl::json debugger_config; + debugger_config["lldb"]["initCommands"] = { + "settings set plugin.jit-loader.gdb.enable on" + }; + debugger_config["interpreter_ptr"] = reinterpret_cast(interpreter.get()); + if (!file_name.empty()) { xeus::xconfiguration config = xeus::load_configuration(file_name); @@ -72,12 +81,14 @@ int main(int argc, char* argv[]) xeus::get_user_name(), std::move(context), std::move(interpreter), - xeus::make_xserver_default, + xeus::make_xserver_shell_main, xeus::make_in_memory_history_manager(), xeus::make_console_logger( xeus::xlogger::msg_type, xeus::make_file_logger(xeus::xlogger::content, "xeus.log") - ) + ), + xcpp::make_cpp_debugger, + debugger_config ); std::clog << "Starting xcpp kernel...\n\n" @@ -94,12 +105,14 @@ int main(int argc, char* argv[]) xeus::get_user_name(), std::move(context), std::move(interpreter), - xeus::make_xserver_default, + xeus::make_xserver_shell_main, xeus::make_in_memory_history_manager(), xeus::make_console_logger( xeus::xlogger::msg_type, xeus::make_file_logger(xeus::xlogger::content, "xeus.log") - ) + ), + xcpp::make_cpp_debugger, + debugger_config ); std::cout << "Getting config" << std::endl; diff --git a/src/xdebugger.cpp b/src/xdebugger.cpp new file mode 100644 index 00000000..9786d15b --- /dev/null +++ b/src/xdebugger.cpp @@ -0,0 +1,723 @@ +/************************************************************************************ + * Copyright (c) 2025, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ +#include "xeus-cpp/xdebugger.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "nlohmann/json.hpp" +#include "xdebuglldb_client.hpp" +#include "xeus-cpp/xinterpreter.hpp" +#include "xeus-zmq/xmiddleware.hpp" +#include "xeus/xsystem.hpp" +#include "xinternal_utils.hpp" + +using namespace std::placeholders; + +namespace xcpp +{ + debugger::debugger( + xeus::xcontext& context, + const xeus::xconfiguration& config, + const std::string& user_name, + const std::string& session_id, + const nl::json& debugger_config + ) + : xdebugger_base(context) + , p_debuglldb_client(new xdebuglldb_client( + context, + config, + xeus::get_socket_linger(), + xdap_tcp_configuration(xeus::dap_tcp_type::client, xeus::dap_init_type::parallel, user_name, session_id), + get_event_callback() + )) + , m_lldb_host("127.0.0.1") + , m_lldb_port("") + , m_debugger_config(debugger_config) + , m_interpreter(reinterpret_cast( + debugger_config["interpreter_ptr"].get() + )) + { + // Register request handlers + // std::cout << "DEBUGGER CONSTRUCTOR" << std::endl; + register_request_handler( + "inspectVariables", + std::bind(&debugger::inspect_variables_request, this, _1), + false + ); + register_request_handler("attach", std::bind(&debugger::attach_request, this, _1), true); + register_request_handler( + "configurationDone", + std::bind(&debugger::configuration_done_request, this, _1), + true + ); + register_request_handler("richInspectVariables", std::bind(&debugger::rich_inspect_variables_request, this, _1), true); + register_request_handler("setBreakpoints", std::bind(&debugger::set_breakpoints_request, this, _1), true); + register_request_handler("dumpCell", std::bind(&debugger::dump_cell_request, this, _1), true); + register_request_handler("copyToGlobals", std::bind(&debugger::copy_to_globals_request, this, _1), true); + register_request_handler("stackTrace", std::bind(&debugger::stack_trace_request, this, _1), true); + register_request_handler("source", std::bind(&debugger::source_request, this, _1), true); + } + + debugger::~debugger() + { + delete p_debuglldb_client; + p_debuglldb_client = nullptr; + } + + bool debugger::start_lldb() + { + jit_process_pid = interpreter::get_current_pid(); + m_lldb_port = xeus::find_free_port(100, 9999, 10099); + + if (m_lldb_port.empty()) + { + std::cerr << "Failed to find a free port for LLDB-DAP" << std::endl; + return false; + } + if (std::getenv("XEUS_LOG")) + { + std::ofstream log("xeus.log", std::ios::app); + log << "===== DEBUGGER CONFIG =====\n"; + log << m_debugger_config.dump(4) << '\n'; + } + + std::vector lldb_args = { + "lldb-dap", + "--connection", + "listen://localhost:" + m_lldb_port + }; + + std::string log_dir = xeus::get_temp_directory_path() + "/xcpp_debug_logs_" + + std::to_string(xeus::get_current_pid()); + xeus::create_directory(log_dir); + std::string log_file = log_dir + "/lldb-dap.log"; + pid_t pid = fork(); + if (pid == 0) + { + int fd = open(log_file.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd != -1) + { + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + close(fd); + } + + int null_fd = open("/dev/null", O_RDONLY); + if (null_fd != -1) + { + dup2(null_fd, STDIN_FILENO); + close(null_fd); + } + std::vector argv; + for (auto& arg : lldb_args) + { + argv.push_back(const_cast(arg.c_str()));; + } + argv.push_back(nullptr); + execvp("lldb-dap", argv.data()); + std::cerr << "Failed to execute lldb-dap" << std::endl; + std::exit(1); + } + else if (pid > 0) + { + int status; + pid_t wait_result = waitpid(pid, &status, WNOHANG); + if (wait_result != 0) + { + std::cerr << "LLDB-DAP process exited prematurely." << std::endl; + return false; + } + m_is_running = true; + m_copy_to_globals_available = true; + return true; + } + else + { + std::cerr << "fork() failed" << std::endl; + return false; + } + } + + bool debugger::start() + { + bool lldb_started = start_lldb(); + if (!lldb_started) + { + std::cerr << "Failed to start LLDB-DAP" << std::endl; + return false; + } + + std::string controller_end_point = xeus::get_controller_end_point("debugger"); + std::string controller_header_end_point = xeus::get_controller_end_point("debugger_header"); + std::string publisher_end_point = xeus::get_publisher_end_point(); + + bind_sockets(controller_header_end_point, controller_end_point); + std::string lldb_endpoint = "tcp://" + m_lldb_host + ":" + m_lldb_port; + std::thread client( + &xdap_tcp_client::start_debugger, + p_debuglldb_client, + lldb_endpoint, + publisher_end_point, + controller_end_point, + controller_header_end_point + ); + client.detach(); + + send_recv_request("REQ"); + std::string tmp_folder = get_tmp_prefix(); + xeus::create_directory(tmp_folder); + return true; + } + + nl::json debugger::attach_request(const nl::json& message) + { + nl::json attach_request = { + {"seq", message["seq"]}, + {"type", "request"}, + {"command", "attach"}, + {"arguments", + { + {"pid", jit_process_pid}, + {"initCommands", {"settings set plugin.jit-loader.gdb.enable on" }} + } + }, + }; + nl::json reply = forward_message(attach_request); + return reply; + } + + nl::json debugger::inspect_variables_request(const nl::json& message) + { + nl::json inspect_request = { + {"seq", message["seq"]}, + {"type", "request"}, + {"command", "variables"}, + {"arguments", {{"variablesReference", 0}}} + }; + nl::json dummy_reply = forward_message(inspect_request); + return dummy_reply; + } + + nl::json debugger::set_breakpoints_request(const nl::json& message) + { + std::string source = message["arguments"]["source"]["path"].get(); + m_breakpoint_list.erase(source); + nl::json bp_json = message["arguments"]["breakpoints"]; + std::vector bp_list(bp_json.begin(), bp_json.end()); + std::clog << "Setting breakpoints in " << source << std::endl; + std::string sequential_source = m_hash_to_sequential[source][0]; + m_breakpoint_list.insert(std::make_pair(std::move(source), std::move(bp_list))); + nl::json mod_message = message; + mod_message["arguments"]["source"]["path"] = sequential_source; + nl::json breakpoint_reply = forward_message(mod_message); + if (breakpoint_reply.contains("body") && breakpoint_reply["body"].contains("breakpoints")) + { + for (auto& bp : breakpoint_reply["body"]["breakpoints"]) + { + if (bp.contains("source") && bp["source"].contains("path")) + { + std::string seq_path = bp["source"]["path"].get(); + if (m_sequential_to_hash.find(seq_path) != m_sequential_to_hash.end()) + { + bp["source"]["path"] = m_sequential_to_hash[seq_path]; + } + } + } + } + return breakpoint_reply; + } + + nl::json debugger::copy_to_globals_request(const nl::json& message) + { + if (!m_copy_to_globals_available) + { + nl::json reply = { + {"type", "response"}, + {"request_seq", message["seq"]}, + {"success", false}, + {"command", message["command"]}, + {"body", "The debugpy version must be greater than or equal 1.6.5 to allow copying a variable to the global scope."} + }; + return reply; + } + + std::string src_var_name = message["arguments"]["srcVariableName"].get(); + std::string dst_var_name = message["arguments"]["dstVariableName"].get(); + int src_frame_id = message["arguments"]["srcFrameId"].get(); + + int seq = message["seq"].get(); + std::string expression = "globals()['" + dst_var_name + "']"; + nl::json request = { + {"type", "request"}, + {"command", "setExpression"}, + {"seq", seq+1}, + {"arguments", { + {"expression", expression}, + {"value", src_var_name}, + {"frameId", src_frame_id} + }} + }; + return forward_message(request); + } + + + nl::json debugger::configuration_done_request(const nl::json& message) + { + nl::json reply = forward_message(message); + return reply; + } + + void debugger::get_container_info(const std::string& var_name, int frame_id, + const std::string& var_type, nl::json& data, nl::json& metadata, int sequence, int size) + { + nl::json container_tree = { + {"type", "container"}, + {"name", var_name}, + {"containerType", var_type}, + {"size", size}, + {"children", nl::json::array()}, + {"expanded", false} + }; + + // Add elements as children + for (int i = 0; i < size; ++i) { + nl::json elem_request = { + {"type", "request"}, + {"command", "evaluate"}, + {"seq", sequence++}, + {"arguments", { + {"expression", var_name + "[" + std::to_string(i) + "]"}, + {"frameId", frame_id}, + {"context", "watch"} + }} + }; + + nl::json elem_reply = forward_message(elem_request); + if (elem_reply["success"].get()) { + std::string elem_value = elem_reply["body"]["result"].get(); + std::string elem_type = elem_reply["body"].contains("type") ? + elem_reply["body"]["type"].get() : "unknown"; + + nl::json child_node = { + {"type", "element"}, + {"name", "[" + std::to_string(i) + "]"}, + {"value", elem_value}, + {"valueType", elem_type}, + {"index", i}, + {"leaf", true} + }; + + // Check if element is also a container + if (is_container_type(elem_type)) { + child_node["leaf"] = false; + child_node["hasChildren"] = true; + int nested_size = 0; + std::smatch match; + std::regex size_regex(R"(size\s*=\s*(\d+))"); + if (std::regex_search(elem_value, match, size_regex) && match.size() > 1) { + nested_size = std::stoi(match[1]); + } + child_node["size"] = nested_size; + } + + container_tree["children"].push_back(child_node); + } + } + data["application/json"] = container_tree; + } + + bool debugger::is_container_type(const std::string& type) const + { + return type.find("std::vector") != std::string::npos || + type.find("std::array") != std::string::npos || + type.find("std::list") != std::string::npos || + type.find("std::deque") != std::string::npos || + type.find("std::map") != std::string::npos || + type.find("std::set") != std::string::npos || + type.find("std::unordered_map") != std::string::npos || + type.find("std::unordered_set") != std::string::npos; + } + + bool debugger::get_variable_info_from_lldb(const std::string& var_name, int frame_id, nl::json& data, nl::json& metadata, int sequence) { + try { + nl::json eval_request = { + {"type", "request"}, + {"command", "evaluate"}, + {"seq", sequence}, + {"arguments", { + {"expression", var_name}, + {"frameId", frame_id}, + {"context", "watch"} + }} + }; + + nl::json eval_reply = forward_message(eval_request); + if (!eval_reply["success"].get()) { + return false; + } + + std::string var_value = eval_reply["body"]["result"].get(); + std::string var_type = eval_reply["body"].contains("type") ? + eval_reply["body"]["type"].get() : "unknown"; + + // Create a unified JSON structure for all variable types + nl::json variable_info = { + {"type", "variable"}, + {"name", var_name}, + {"value", var_value}, + {"valueType", var_type}, + {"frameId", frame_id}, + {"timestamp", std::time(nullptr)} + }; + + if(is_container_type(var_type)) { + // Extract size from var_value, e.g., "size=2" + int size = 0; + std::smatch match; + std::regex size_regex(R"(size\s*=\s*(\d+))"); + if (std::regex_search(var_value, match, size_regex) && match.size() > 1) { + size = std::stoi(match[1]); + } + + variable_info["isContainer"] = true; + variable_info["size"] = size; + variable_info["leaf"] = false; + variable_info["hasChildren"] = size > 0; + + // Get detailed container info + get_container_info(var_name, frame_id, var_type, data, metadata, sequence + 1, size); + + // Add container summary to the main variable info + if (data["application/json"].contains("children")) { + variable_info["children"] = data["application/json"]["children"]; + variable_info["expanded"] = false; + } + } else { + variable_info["isContainer"] = false; + variable_info["leaf"] = true; + variable_info["hasChildren"] = false; + } + + // Store everything in application/json format + data["application/json"] = variable_info; + + // Metadata for the frontend renderer + metadata["application/json"] = { + {"version", "1.0"}, + {"renderer", "variable-inspector"}, + {"expandable", variable_info["hasChildren"]}, + {"rootVariable", var_name}, + {"capabilities", { + {"tree", true}, + {"search", true}, + {"filter", true}, + {"export", true} + }} + }; + + return true; + } catch (const std::exception& e) { + return false; + } + } + + nl::json debugger::rich_inspect_variables_request(const nl::json& message) + { + nl::json reply = { + {"type", "response"}, + {"request_seq", message["seq"]}, + {"success", false}, + {"command", message["command"]} + }; + + std::string var_name; + try { + var_name = message["arguments"]["variableName"].get(); + } catch (const nl::json::exception& e) { + reply["body"] = { + {"data", { + {"application/json", { + {"type", "error"}, + {"message", "Invalid variable name in request"}, + {"details", std::string(e.what())} + }} + }}, + {"metadata", { + {"application/json", { + {"error", true}, + {"errorType", "invalid_request"} + }} + }} + }; + return reply; + } + + int frame_id = 0; + if(message["arguments"].contains("frameId")) { + frame_id = message["arguments"]["frameId"].get(); + } + + nl::json var_data; + nl::json var_metadata; + bool success = false; + + auto stopped_threads = base_type::get_stopped_threads(); + if(!stopped_threads.empty()) { + success = get_variable_info_from_lldb(var_name, frame_id, var_data, var_metadata, message["seq"].get() + 1); + } else { + // When there is no breakpoint hit, return a placeholder structure + var_data["application/json"] = { + {"type", "unavailable"}, + {"name", var_name}, + {"message", "Variable not accessible - no active debugging session"}, + {"suggestions", { + "Set a breakpoint and run the program", + "Ensure the variable is in scope", + "Check if the program is currently stopped" + }} + }; + var_metadata["application/json"] = { + {"available", false}, + {"reason", "no_stopped_threads"} + }; + success = true; + } + + if(success) { + reply["body"] = { + {"data", var_data}, + {"metadata", var_metadata} + }; + reply["success"] = true; + } else { + reply["body"] = { + {"data", { + {"application/json", { + {"type", "error"}, + {"name", var_name}, + {"message", "Variable '" + var_name + "' not found or not accessible"}, + {"frameId", frame_id}, + {"suggestions", { + "Check variable name spelling", + "Ensure variable is in current scope", + "Verify the frame ID is correct" + }} + }} + }}, + {"metadata", { + {"application/json", { + {"error", true}, + {"errorType", "variable_not_found"} + }} + }} + }; + } + return reply; + } + + nl::json debugger::stack_trace_request(const nl::json& message) + { + int requested_thread_id = message["arguments"]["threadId"]; + auto stopped_threads = base_type::get_stopped_threads(); + + nl::json reply = forward_message(message); + + if (!reply.contains("body") || !reply["body"].contains("stackFrames")) { + return reply; + } + + auto& stack_frames = reply["body"]["stackFrames"]; + nl::json filtered_frames = nl::json::array(); + + for (auto& frame : stack_frames) + { + if (frame.contains("source") && frame["source"].contains("path")) + { + std::string path = frame["source"]["path"]; + std::string name = frame["source"]["name"]; + // Check if path is in the format input_line_ + if (name.find("input_line_") != std::string::npos) + { + // Map sequential filename to hash if mapping exists + auto it = m_sequential_to_hash.find(name); + if (it != m_sequential_to_hash.end()) + { + frame["source"]["path"] = it->second; + // Set name to last part of path (filename) + std::string filename = it->second.substr(it->second.find_last_of("/\\") + 1); + frame["source"]["name"] = filename; + } else { + std::string code, hash_file_name; + // Extract execution count number from "input_line_" + int exec_count = -1; + std::string prefix = "input_line_"; + if (name.compare(0, prefix.size(), prefix) == 0) + { + try + { + exec_count = std::stoi(name.substr(prefix.size())); + } + catch (...) + { + exec_count = 0; + } + } + + if (exec_count != -1) { + code = m_interpreter->get_code_from_execution_count(exec_count - 1); + hash_file_name = get_cell_temporary_file(code); + frame["source"]["path"] = hash_file_name; + frame["source"]["name"] = hash_file_name.substr(hash_file_name.find_last_of("/\\") + 1); + // Update mappings if not already present + m_hash_to_sequential[hash_file_name].push_back(name); + m_sequential_to_hash[name] = hash_file_name; + } + } + filtered_frames.push_back(frame); + } + } + } + + reply["body"]["stackFrames"] = filtered_frames; + return reply; + } + + void debugger::stop() + { + // std::cout << "STOP" << std::endl; + std::string controller_end_point = xeus::get_controller_end_point("debugger"); + std::string controller_header_end_point = xeus::get_controller_end_point("debugger_header"); + unbind_sockets(controller_header_end_point, controller_end_point); + } + + xeus::xdebugger_info debugger::get_debugger_info() const + { + return xeus::xdebugger_info( + xeus::get_tmp_hash_seed(), + get_tmp_prefix(), + get_tmp_suffix(), + true, // Supports exceptions + {"C++ Exceptions"}, + true // Supports breakpoints + ); + } + + nl::json debugger::dump_cell_request(const nl::json& message) + { + std::string code; + try + { + code = message["arguments"]["code"].get(); + // // std::cout << "Execution Count is: " << m_interpreter->get_execution_count(code)[0] << std::endl; + } + catch (nl::json::type_error& e) + { + std::clog << e.what() << std::endl; + } + catch (...) + { + std::clog << "XDEBUGGER: Unknown issue" << std::endl; + } + std::string hash_file_name = get_cell_temporary_file(code); + for(auto& exec_count: m_interpreter->get_execution_count(code)) + { + m_hash_to_sequential[hash_file_name].push_back("input_line_" + std::to_string(exec_count + 1)); + m_sequential_to_hash["input_line_" + std::to_string(exec_count + 1)] = hash_file_name; + std::clog << "Mapping: " << hash_file_name << " <-> " << "input_line_" + std::to_string(exec_count + 1) << std::endl; + } + std::fstream fs(hash_file_name, std::ios::in); + if (!fs.is_open()) + { + fs.clear(); + fs.open(hash_file_name, std::ios::out); + fs << code; + } + + nl::json reply = { + {"type", "response"}, + {"request_seq", message["seq"]}, + {"success", true}, + {"command", message["command"]}, + {"body", {{"sourcePath", hash_file_name}}} + }; + return reply; + } + + nl::json debugger::source_request(const nl::json& message) + { + std::string sourcePath; + try + { + sourcePath = message["arguments"]["source"]["path"].get(); + } + catch(nl::json::type_error& e) + { + std::clog << e.what() << std::endl; + } + catch(...) + { + std::clog << "XDEBUGGER: Unknown issue" << std::endl; + } + + std::ifstream ifs(sourcePath, std::ios::in); + if(!ifs.is_open()) + { + nl::json reply = { + {"type", "response"}, + {"request_seq", message["seq"]}, + {"success", false}, + {"command", message["command"]}, + {"message", "source unavailable"}, + {"body", {{}}} + }; + return reply; + } + + std::string content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + + nl::json reply = { + {"type", "response"}, + {"request_seq", message["seq"]}, + {"success", true}, + {"command", message["command"]}, + {"body", { + {"content", content} + }} + }; + return reply; + } + + std::string debugger::get_cell_temporary_file(const std::string& code) const + { + return get_cell_tmp_file(code); + } + + std::unique_ptr make_cpp_debugger( + xeus::xcontext& context, + const xeus::xconfiguration& config, + const std::string& user_name, + const std::string& session_id, + const nl::json& debugger_config + ) + { + return std::unique_ptr( + new debugger(context, config, user_name, session_id, debugger_config) + ); + } +} \ No newline at end of file diff --git a/src/xdebuglldb_client.cpp b/src/xdebuglldb_client.cpp new file mode 100644 index 00000000..9664c7d9 --- /dev/null +++ b/src/xdebuglldb_client.cpp @@ -0,0 +1,65 @@ +/************************************************************************************ + * Copyright (c) 2025, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ +#include "xdebuglldb_client.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "nlohmann/json.hpp" +#include "xeus/xmessage.hpp" +#include +#include + +namespace nl = nlohmann; + +namespace xcpp +{ + xdebuglldb_client::xdebuglldb_client( + xeus::xcontext& context, + const xeus::xconfiguration& config, + int socket_linger, + const xdap_tcp_configuration& dap_config, + const event_callback& cb + ) + : base_type(context, config, socket_linger, dap_config, cb) + { + std::cout << "xdebuglldb_client initialized" << std::endl; + } + + + void xdebuglldb_client::handle_event(nl::json message) + { + forward_event(std::move(message)); + } + + nl::json xdebuglldb_client::get_stack_frames(int thread_id, int seq) + { + nl::json request = { + {"type", "request"}, + {"seq", seq}, + {"command", "stackTrace"}, + {"arguments", {{"threadId", thread_id}}} + }; + + send_dap_request(std::move(request)); + + nl::json reply = wait_for_message( + [](const nl::json& message) + { + return message["type"] == "response" && message["command"] == "stackTrace"; + } + ); + + return reply["body"]["stackFrames"]; + } +} diff --git a/src/xdebuglldb_client.hpp b/src/xdebuglldb_client.hpp new file mode 100644 index 00000000..fb3aea70 --- /dev/null +++ b/src/xdebuglldb_client.hpp @@ -0,0 +1,40 @@ +/************************************************************************************ + * Copyright (c) 2025, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ +#ifndef XEUS_CPP_DEBUGLLDB_CLIENT_HPP +#define XEUS_CPP_DEBUGLLDB_CLIENT_HPP + +#include "xeus-zmq/xdap_tcp_client.hpp" + +namespace xcpp +{ + using xeus::xdap_tcp_client; + using xeus::xdap_tcp_configuration; + + class xdebuglldb_client : public xdap_tcp_client + { + public: + + using base_type = xdap_tcp_client; + using event_callback = base_type::event_callback; + + xdebuglldb_client(xeus::xcontext& context, + const xeus::xconfiguration& config, + int socket_linger, + const xdap_tcp_configuration& dap_config, + const event_callback& cb); + + virtual ~xdebuglldb_client() = default; + + private: + + void handle_event(nl::json message) override; + nl::json get_stack_frames(int thread_id, int seq); + }; +} + +#endif diff --git a/src/xinternal_utils.cpp b/src/xinternal_utils.cpp new file mode 100644 index 00000000..3b63ae09 --- /dev/null +++ b/src/xinternal_utils.cpp @@ -0,0 +1,34 @@ +/************************************************************************************ + * Copyright (c) 2025, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ +#include "xinternal_utils.hpp" + +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace xcpp +{ + std::string get_tmp_prefix() + { + return xeus::get_tmp_prefix("xcpp"); + } + + std::string get_tmp_suffix() + { + return ".cpp"; + } + + std::string get_cell_tmp_file(const std::string& content) + { + return xeus::get_cell_tmp_file(get_tmp_prefix(), + content, + get_tmp_suffix()); + } +} diff --git a/src/xinternal_utils.hpp b/src/xinternal_utils.hpp new file mode 100644 index 00000000..9a70b970 --- /dev/null +++ b/src/xinternal_utils.hpp @@ -0,0 +1,23 @@ +/************************************************************************************ + * Copyright (c) 2025, xeus-cpp contributors * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ************************************************************************************/ +#ifndef XEUS_CPP_INTERNAL_UTILS_HPP +#define XEUS_CPP_INTERNAL_UTILS_HPP + +#include + +#include "xeus/xsystem.hpp" + +namespace xcpp +{ + // Temporary file utilities for Jupyter cells + std::string get_tmp_prefix(); + std::string get_tmp_suffix(); + std::string get_cell_tmp_file(const std::string& content); +} + +#endif diff --git a/src/xinterpreter.cpp b/src/xinterpreter.cpp index 14972324..e5537166 100644 --- a/src/xinterpreter.cpp +++ b/src/xinterpreter.cpp @@ -24,6 +24,10 @@ #endif #include "xparser.hpp" #include "xsystem.hpp" +#ifndef WIN32 +#include +#include +#endif using Args = std::vector; @@ -73,6 +77,11 @@ namespace xcpp xeus::register_interpreter(this); } + int32_t interpreter::get_current_pid() + { + return Cpp::GetExecutorPID(); + } + static std::string get_stdopt() { // We need to find what's the C++ version the interpreter runs with. @@ -109,7 +118,7 @@ __get_cxx_version () { //NOLINTNEXTLINE (cppcoreguidelines-pro-bounds-pointer-arithmetic) createInterpreter(Args(argv ? argv + 1 : argv, argv + argc)); - m_version = get_stdopt(); + // m_version = get_stdopt(); redirect_output(); init_preamble(); init_magic(); @@ -122,7 +131,7 @@ __get_cxx_version () void interpreter::execute_request_impl( send_reply_callback cb, - int /*execution_count*/, + int execution_count, const std::string& code, xeus::execute_request_config config, nl::json /*user_expressions*/ @@ -164,6 +173,10 @@ __get_cxx_version () std::string err; + m_code_to_execution_count_map[code].push_back(execution_count); + m_execution_count_to_code_map[execution_count] = code; + std::clog << "In [" << execution_count << "]: " << code << std::endl; + // Attempt normal evaluation try { @@ -312,6 +325,7 @@ __get_cxx_version () "\n" " xeus-cpp: a C++ Jupyter kernel - based on Clang-repl\n"; result["banner"] = banner; + result["debugger"] = true; result["language_info"]["name"] = "C++"; result["language_info"]["version"] = m_version; result["language_info"]["mimetype"] = "text/x-c++src"; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 54869f4a..f2c9d998 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -46,6 +46,9 @@ find_package(doctest) set(XEUS_CPP_TESTS main.cpp test_interpreter.cpp + test_debugger.cpp + xeus_client.hpp + xeus_client.cpp ) add_executable(test_xeus_cpp ${XEUS_CPP_TESTS}) @@ -114,7 +117,7 @@ else() INSTALL_RPATH_USE_LINK_PATH TRUE ) - target_link_libraries(test_xeus_cpp xeus-cpp doctest::doctest ${CMAKE_THREAD_LIBS_INIT}) + target_link_libraries(test_xeus_cpp xeus-cpp xeus-zmq doctest::doctest ${CMAKE_THREAD_LIBS_INIT}) target_include_directories(test_xeus_cpp PRIVATE ${XEUS_CPP_INCLUDE_DIR}) add_custom_target(xtest COMMAND test_xeus_cpp DEPENDS test_xeus_cpp) diff --git a/test/debugger/__init__.py b/test/debugger/__init__.py new file mode 100644 index 00000000..5619d37b --- /dev/null +++ b/test/debugger/__init__.py @@ -0,0 +1 @@ +"""Test package for xeus-cpp debugger""" diff --git a/test/debugger/temp.json b/test/debugger/temp.json new file mode 100644 index 00000000..f6610cad --- /dev/null +++ b/test/debugger/temp.json @@ -0,0 +1,16 @@ +{'header': {'date': datetime.datetime(2025, + 10, + 18, + 14, + 27, + 51, + 315667, tzinfo=tzutc()), 'msg_id': 'dab29c56c1564118969e17dd57368818', 'msg_type': 'kernel_info_reply', 'session': '8fc3e1ee796e4bc09e668f57f6c1275e', 'username': 'abhinav', 'version': '5.3' + }, 'msg_id': 'dab29c56c1564118969e17dd57368818', 'msg_type': 'kernel_info_reply', 'parent_header': {'date': datetime.datetime(2025, + 10, + 18, + 14, + 27, + 51, + 315437, tzinfo=tzutc()), 'msg_id': '875bed24-938f5cb8f579d7c5e04515f7_29098_3', 'msg_type': 'kernel_info_request', 'session': '875bed24-938f5cb8f579d7c5e04515f7', 'username': 'abhinav', 'version': '5.3' + }, 'metadata': {}, 'content': {'banner': ' __ _____ _ _ ___\n \\ \\/ / _ \\ | | / __|\n > < __/ |_| \\__ \\\n /_/\\_\\___|\\__,_|___/\n\n xeus-cpp: a C++ Jupyter kernel - based on Clang-repl\n', 'debugger': True, 'help_links': [ + {'text': 'Xeus-cpp Reference', 'url': 'https: //xeus-cpp.readthedocs.io'}], 'implementation': 'xeus-cpp', 'implementation_version': '0.7.2', 'language_info': {'codemirror_mode': 'text/x-c++src', 'file_extension': '.cpp', 'mimetype': 'text/x-c++src', 'name': 'C++', 'version': ''}, 'protocol_version': '5.3', 'status': 'ok'}, 'buffers': []} \ No newline at end of file diff --git a/test/debugger/test_debugger.py b/test/debugger/test_debugger.py new file mode 100644 index 00000000..fa2c86c3 --- /dev/null +++ b/test/debugger/test_debugger.py @@ -0,0 +1,336 @@ +"""Test xeus-cpp debugger functionality using LLDB""" + +import sys +import pytest +import time +from queue import Empty + +from util import TIMEOUT, get_replies, get_reply, new_kernel + +seq = 0 + +# xeus-cpp uses LLDB for debugging, not debugpy +# Test if xeus-cpp kernel with debugger support is available +try: + from jupyter_client import kernelspec + ksm = kernelspec.KernelSpecManager() + available_kernels = ksm.find_kernel_specs() + print(f"Available kernels: {available_kernels}") + HAS_XCPP_DEBUGGER = any('xcpp17-debugger' in k for k in available_kernels.keys()) + print(f"xeus-cpp debugger available: {HAS_XCPP_DEBUGGER}") +except Exception: + HAS_XCPP_DEBUGGER = False + + +def send_debug_request(kernel, command, arguments=None, timeout=TIMEOUT): + """Send a debug request and get reply""" + global seq + seq += 1 + + msg = kernel.session.msg( + "debug_request", + { + "type": "request", + "seq": seq, + "command": command, + "arguments": arguments or {}, + }, + ) + + kernel.control_channel.send(msg) + reply = get_reply(kernel, msg["header"]["msg_id"], channel="control", timeout=timeout) + return reply.get("content", {}) + +@pytest.fixture() +def kernel(): + """Create a fresh xeus-cpp kernel""" + with new_kernel(kernel_name="xcpp17-debugger") as kc: + # Give kernel time to fully start + time.sleep(1) + yield kc + + +def test_kernel_starts(): + """Test that kernel can start""" + with new_kernel(kernel_name="xcpp17-debugger") as kc: + # Execute simple code + msg_id = kc.execute("int test = 1;") + reply = kc.get_shell_msg(timeout=TIMEOUT) + assert reply["content"]["status"] == "ok" + print("✓ Kernel starts and executes code") + + +# def test_debug_initialize(): +# """Test debugger initialization""" +# with new_kernel(kernel_name="xcpp17-debugger") as kc: +# # Execute some code first to ensure interpreter is ready +# kc.execute("int x = 42;") +# time.sleep(0.5) + +# reply = send_debug_request( +# kc, +# "initialize", +# { +# "clientID": "test-client", +# "clientName": "testClient", +# "adapterID": "lldb", +# "pathFormat": "path", +# "linesStartAt1": True, +# "columnsStartAt1": True, +# "supportsVariableType": True, +# "supportsVariablePaging": False, +# "supportsRunInTerminalRequest": False, +# "locale": "en", +# }, +# ) + +# print(f"Initialize reply: {reply}") + +# # Check we got a response +# assert isinstance(reply, dict) +# assert "type" in reply or "success" in reply + +# if reply.get("success"): +# print("✓ Debugger initialized successfully") +# else: +# print(f"⚠ Initialize returned: {reply}") + + +# def test_debug_attach_with_launch(): +# """Test debugger attachment using launch method""" +# with new_kernel(kernel_name="xcpp17-debugger") as kc: +# # Execute code to ensure process is active +# kc.execute("#include ") +# time.sleep(0.5) + +# # Initialize +# init_reply = send_debug_request( +# kc, +# "initialize", +# { +# "clientID": "test-client", +# "adapterID": "lldb", +# "pathFormat": "path", +# "linesStartAt1": True, +# "columnsStartAt1": True, +# }, +# ) + +# print(f"Initialize: {init_reply.get('success', False)}") + +# if not init_reply.get("success"): +# pytest.skip(f"Initialize failed: {init_reply}") + +# # Try launch instead of attach +# launch_reply = send_debug_request( +# kc, +# "launch", +# { +# "stopOnEntry": False, +# "justMyCode": False, +# } +# ) + +# print(f"Launch reply: {launch_reply}") + +# if launch_reply.get("success"): +# print("✓ Debugger launched successfully") +# else: +# # Try attach as fallback +# attach_reply = send_debug_request( +# kc, +# "attach", +# { +# "stopOnEntry": False, +# } +# ) +# print(f"Attach reply: {attach_reply}") + +# if not attach_reply.get("success"): +# error = attach_reply.get("body", {}).get("error", {}) +# pytest.skip(f"Could not attach: {error.get('format', attach_reply)}") + + +# def test_dump_cell(): +# """Test dumping cell code""" +# with new_kernel(kernel_name="xcpp17-debugger") as kc: +# # Initialize debugger first +# kc.execute("int dummy = 0;") +# time.sleep(0.5) + +# init_reply = send_debug_request( +# kc, +# "initialize", +# { +# "clientID": "test-client", +# "clientName": "testClient", +# "adapterID": "lldb", +# "pathFormat": "path", +# "linesStartAt1": True, +# "columnsStartAt1": True, +# "supportsVariableType": True, +# "supportsVariablePaging": False, +# "supportsRunInTerminalRequest": False, +# "locale": "en", +# }, +# ) + +# print(f"Initialize reply: {init_reply}") + +# if not init_reply.get("success"): +# pytest.skip("Initialize failed") + +# # Dump a cell +# code = """#include +# int add(int a, int b) { +# return a + b; +# } +# int result = add(2, 3);""" + +# dump_reply = send_debug_request( +# kc, +# "dumpCell", +# {"code": code} +# ) + +# # print(f"DumpCell reply: {dump_reply}") + +# if dump_reply.get("success"): +# source_path = dump_reply.get("body", {}).get("sourcePath") +# print(f"✓ Cell dumped to: {source_path}") +# assert source_path is not None +# else: +# print(f"⚠ DumpCell failed: {dump_reply}") + + +# def test_configuration_done(): +# """Test configuration done message""" +# with new_kernel(kernel_name="xcpp17-debugger") as kc: +# kc.execute("int x = 1;") +# time.sleep(0.5) + +# # Initialize +# init_reply = send_debug_request(kc, "initialize", +# { +# "clientID": "test-client", +# "clientName": "testClient", +# "adapterID": "lldb", +# "pathFormat": "path", +# "linesStartAt1": True, +# "columnsStartAt1": True, +# "supportsVariableType": True, +# "supportsVariablePaging": False, +# "supportsRunInTerminalRequest": False, +# "locale": "en", +# }) + +# if not init_reply.get("success"): +# pytest.skip("Initialize failed") + +# # Launch +# launch_reply = send_debug_request(kc, "attach", { +# "stopOnEntry": False, +# }) + +# if not launch_reply.get("success"): +# pytest.skip("Launch failed") + +# # Configuration done +# config_reply = send_debug_request(kc, "configurationDone") + +# print(f"ConfigurationDone reply: {config_reply}") + +# assert isinstance(config_reply, dict) +# print("✓ Configuration done sent") + + +def test_breakpoint_without_execution(): + """Test setting breakpoint (without execution)""" + with new_kernel(kernel_name="xcpp17-debugger") as kc: + kc.execute("int x = 1;") + time.sleep(0.5) + + # Initialize + init_reply = send_debug_request(kc, "initialize", + { + "clientID": "test-client", + "clientName": "testClient", + "adapterID": "lldb", + "pathFormat": "path", + "linesStartAt1": True, + "columnsStartAt1": True, + "supportsVariableType": True, + "supportsVariablePaging": False, + "supportsRunInTerminalRequest": False, + "locale": "en", + }) + + if not init_reply.get("success"): + pytest.skip("Initialize failed") + + # Launch + launch_reply = send_debug_request(kc, "attach", { + "stopOnEntry": False, + }) + + if not launch_reply.get("success"): + pytest.skip("Attach failed") + + # Dump cell + code = """int multiply(int a, int b) { + int result = a * b; + return result; +}""" + # execute code to have something in the interpreter + kc.execute(code) + time.sleep(0.5) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + + if not dump_reply.get("success"): + pytest.skip("DumpCell failed") + + source = dump_reply["body"]["sourcePath"] + + print(f"Dumped source path: {source}") + + # Set breakpoint + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source}, + "breakpoints": [{"line": 2}], + "sourceModified": False, + } + ) + + print(f"SetBreakpoints reply: {bp_reply}") + + if bp_reply.get("success"): + breakpoints = bp_reply.get("body", {}).get("breakpoints", []) + print(f"✓ Set {len(breakpoints)} breakpoint(s)") + if breakpoints: + print(f" Verified: {breakpoints[0].get('verified', False)}") + else: + print(f"⚠ SetBreakpoints failed: {bp_reply}") + + +# @pytest.mark.skipif(not HAS_XCPP, reason="xeus-cpp kernel not available") +# def test_kernel_info_debugger_support(): +# """Check if kernel advertises debugger support""" +# with new_kernel(kernel_name="xcpp17") as kc: +# kc.kernel_info() +# reply = kc.get_shell_msg(timeout=TIMEOUT) + +# features = reply["content"].get("supported_features", []) +# print(f"Supported features: {features}") + +# if "debugger" in features: +# print("✓ Kernel advertises debugger support") +# else: +# print("⚠ Debugger not in supported features (may still work)") + + +if __name__ == "__main__": + pytest.main([__file__, "-v", "-s"]) \ No newline at end of file diff --git a/test/debugger/util.py b/test/debugger/util.py new file mode 100644 index 00000000..d424cf33 --- /dev/null +++ b/test/debugger/util.py @@ -0,0 +1,332 @@ +"""utilities for testing xeus-cpp kernels""" + +from __future__ import annotations + +import atexit +import os +import sys +from contextlib import contextmanager +from queue import Empty +from subprocess import STDOUT +from tempfile import TemporaryDirectory +from time import time + +from jupyter_client import manager +from jupyter_client.blocking.client import BlockingKernelClient + +STARTUP_TIMEOUT = 60 +TIMEOUT = 100 + +KM: manager.KernelManager = None # type:ignore +KC: BlockingKernelClient = None # type:ignore + + +def start_new_kernel(**kwargs): + """start a new xeus-cpp kernel, and return its Manager and Client + + Integrates with our output capturing for tests. + """ + kwargs["stderr"] = STDOUT + # Specify xeus-cpp kernel + kwargs.setdefault("kernel_name", "xcpp17-debugger") + return manager.start_new_kernel(startup_timeout=STARTUP_TIMEOUT, **kwargs) + + +def flush_channels(kc=None): + """flush any messages waiting on the queue""" + if kc is None: + kc = KC + for get_msg in (kc.get_shell_msg, kc.get_iopub_msg): + while True: + try: + msg = get_msg(timeout=0.1) + except Empty: + break + + +def get_reply(kc, msg_id, timeout=TIMEOUT, channel="shell"): + """Get a reply for a specific message ID""" + t0 = time() + while True: + get_msg = getattr(kc, f"get_{channel}_msg") + reply = get_msg(timeout=timeout) + if reply["parent_header"]["msg_id"] == msg_id: + break + # Allow debugging ignored replies + print(f"Ignoring reply not to {msg_id}: {reply}") + t1 = time() + timeout -= t1 - t0 + t0 = t1 + return reply + + +def get_replies(kc, msg_ids: list[str], timeout=TIMEOUT, channel="shell"): + """Get replies which may arrive in any order""" + count = 0 + replies = [None] * len(msg_ids) + while count < len(msg_ids): + get_msg = getattr(kc, f"get_{channel}_msg") + reply = get_msg(timeout=timeout) + try: + msg_id = reply["parent_header"]["msg_id"] + replies[msg_ids.index(msg_id)] = reply + count += 1 + except ValueError: + print(f"Ignoring reply not to any of {msg_ids}: {reply}") + return replies + + +def execute(code="", kc=None, **kwargs): + """Execute C++ code and validate the execution request + + Args: + code: C++ code to execute + kc: kernel client (uses global KC if None) + **kwargs: additional arguments for kc.execute() + + Returns: + tuple: (msg_id, reply_content) + """ + if kc is None: + kc = KC + msg_id = kc.execute(code=code, **kwargs) + reply = get_reply(kc, msg_id, TIMEOUT) + + # Validate it's an execute_reply + assert reply["msg_type"] == "execute_reply", f"Expected execute_reply, got {reply['msg_type']}" + + busy = kc.get_iopub_msg(timeout=TIMEOUT) + assert busy["msg_type"] == "status" + assert busy["content"]["execution_state"] == "busy" + + if not kwargs.get("silent"): + execute_input = kc.get_iopub_msg(timeout=TIMEOUT) + assert execute_input["msg_type"] == "execute_input" + assert execute_input["content"]["code"] == code + + # show tracebacks/errors if present for debugging + if reply["content"].get("traceback"): + print("\n".join(reply["content"]["traceback"]), file=sys.stderr) + + if reply["content"]["status"] == "error": + print(f"Error: {reply['content'].get('ename', 'Unknown')}", file=sys.stderr) + print(f"Message: {reply['content'].get('evalue', '')}", file=sys.stderr) + + return msg_id, reply["content"] + + +def start_global_kernel(kernel_name="xcpp17"): + """start the global kernel (if it isn't running) and return its client + + Args: + kernel_name: xeus-cpp kernel variant (xcpp11, xcpp14, xcpp17, xcpp20) + """ + global KM, KC + if KM is None: + KM, KC = start_new_kernel(kernel_name=kernel_name) + atexit.register(stop_global_kernel) + else: + flush_channels(KC) + return KC + + +@contextmanager +def kernel(kernel_name="xcpp17"): + """Context manager for the global kernel instance + + Should be used for most kernel tests + + Args: + kernel_name: xeus-cpp kernel variant (xcpp11, xcpp14, xcpp17, xcpp20) + + Yields: + kernel_client: connected KernelClient instance + """ + yield start_global_kernel(kernel_name=kernel_name) + + +def uses_kernel(test_f): + """Decorator for tests that use the global kernel""" + + def wrapped_test(): + with kernel() as kc: + test_f(kc) + + wrapped_test.__doc__ = test_f.__doc__ + wrapped_test.__name__ = test_f.__name__ + return wrapped_test + + +def stop_global_kernel(): + """Stop the global shared kernel instance, if it exists""" + global KM, KC + if KC is not None: + KC.stop_channels() + KC = None # type:ignore + if KM is not None: + KM.shutdown_kernel(now=True) + KM = None # type:ignore + + +@contextmanager +def new_kernel(argv=None, kernel_name="xcpp17"): + """Context manager for a new kernel in a subprocess + + Should only be used for tests where the kernel must not be reused. + + Args: + argv: additional command-line arguments + kernel_name: xeus-cpp kernel variant (xcpp11, xcpp14, xcpp17, xcpp20) + + Yields: + kernel_client: connected KernelClient instance + """ + kwargs = {"stderr": STDOUT, "kernel_name": kernel_name} + if argv is not None: + kwargs["extra_arguments"] = argv + with manager.run_kernel(**kwargs) as kc: + yield kc + + +def assemble_output(get_msg, timeout=1, parent_msg_id: str | None = None): + """Assemble stdout/stderr from an execution + + Args: + get_msg: function to get messages (usually kc.get_iopub_msg) + timeout: timeout for getting messages + parent_msg_id: filter messages by parent message ID + + Returns: + tuple: (stdout, stderr) + """ + stdout = "" + stderr = "" + while True: + try: + msg = get_msg(timeout=timeout) + except Empty: + break + + msg_type = msg["msg_type"] + content = msg["content"] + + if parent_msg_id is not None and msg["parent_header"]["msg_id"] != parent_msg_id: + continue + + if msg_type == "status" and content["execution_state"] == "idle": + break + elif msg_type == "stream": + if content["name"] == "stdout": + stdout += content["text"] + elif content["name"] == "stderr": + stderr += content["text"] + else: + raise KeyError("bad stream: %r" % content["name"]) + + return stdout, stderr + + +def wait_for_idle(kc, parent_msg_id: str | None = None, timeout=TIMEOUT): + """Wait for kernel to become idle + + Args: + kc: kernel client + parent_msg_id: optional parent message ID to match + timeout: maximum time to wait + """ + start_time = time() + while True: + elapsed = time() - start_time + if elapsed > timeout: + raise TimeoutError(f"Kernel did not become idle within {timeout}s") + + msg = kc.get_iopub_msg(timeout=min(1, timeout - elapsed)) + msg_type = msg["msg_type"] + content = msg["content"] + if ( + msg_type == "status" + and content["execution_state"] == "idle" + and (parent_msg_id is None or msg["parent_header"]["msg_id"] == parent_msg_id) + ): + break + + +class TemporaryWorkingDirectory(TemporaryDirectory): + """ + Creates a temporary directory and sets the cwd to that directory. + Automatically reverts to previous cwd upon cleanup. + + Usage example: + with TemporaryWorkingDirectory() as tmpdir: + ... + """ + + def __enter__(self): + self.old_wd = os.getcwd() + os.chdir(self.name) + return super().__enter__() + + def __exit__(self, exc, value, tb): + os.chdir(self.old_wd) + return super().__exit__(exc, value, tb) + + +# C++-specific helper functions + +def compile_and_run(code: str, kc=None, includes: list[str] = None, + flags: list[str] = None) -> tuple[str, dict]: + """Helper to compile and run C++ code with common setup + + Args: + code: C++ code to execute + kc: kernel client + includes: list of headers to include + flags: compiler flags to set + + Returns: + tuple: (msg_id, reply_content) + """ + if kc is None: + kc = KC + + full_code = "" + + # Add includes + if includes: + for inc in includes: + full_code += f"#include <{inc}>\n" + full_code += "\n" + + # Add compiler flags if needed + if flags: + for flag in flags: + full_code += f"#pragma cling add_compiler_flag {flag}\n" + full_code += "\n" + + full_code += code + + return execute(full_code, kc) + + +def check_cpp_version(kc=None) -> str: + """Check which C++ standard is being used""" + if kc is None: + kc = KC + + code = """ + #include + std::cout << __cplusplus << std::endl; + """ + + msg_id, reply = execute(code, kc) + stdout, _ = assemble_output(kc.get_iopub_msg, parent_msg_id=msg_id) + + version_map = { + "199711": "C++98", + "201103": "C++11", + "201402": "C++14", + "201703": "C++17", + "202002": "C++20", + } + + return version_map.get(stdout.strip(), f"Unknown ({stdout.strip()})") \ No newline at end of file From 05da59f804744a89cb6cae4cbf79f378828bc706 Mon Sep 17 00:00:00 2001 From: kr-2003 Date: Thu, 30 Oct 2025 23:53:15 +0530 Subject: [PATCH 2/2] xeus-cpp debugger with testing --- notebooks/xeus-cpp_output.ipynb | 1202 +++++++++++++++++++++++++++++++ test/CMakeLists.txt | 3 - test/debugger/__init__.py | 1 - test/debugger/temp.json | 16 - test/debugger/test_debugger.py | 336 --------- test/debugger/util.py | 332 --------- test/test_debugger.py | 512 +++++++++++++ 7 files changed, 1714 insertions(+), 688 deletions(-) create mode 100644 notebooks/xeus-cpp_output.ipynb delete mode 100644 test/debugger/__init__.py delete mode 100644 test/debugger/temp.json delete mode 100644 test/debugger/test_debugger.py delete mode 100644 test/debugger/util.py create mode 100644 test/test_debugger.py diff --git a/notebooks/xeus-cpp_output.ipynb b/notebooks/xeus-cpp_output.ipynb new file mode 100644 index 00000000..a179ba03 --- /dev/null +++ b/notebooks/xeus-cpp_output.ipynb @@ -0,0 +1,1202 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7afcde8b", + "metadata": { + "tags": [ + "papermill-error-cell-tag" + ] + }, + "source": [ + "An Exception was encountered at 'In [4]'." + ] + }, + { + "cell_type": "markdown", + "id": "aa9d4f33-5bd8-42f2-918a-e654d4363577", + "metadata": { + "papermill": { + "duration": 0.012522, + "end_time": "2025-10-30T18:09:12.375607", + "exception": false, + "start_time": "2025-10-30T18:09:12.363085", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "
\n", + "

C++ kernel based on xeus

\n", + "
\n", + "\n", + "A Jupyter kernel for C++ based on the `CppInterOp`, a clang based C++ Interoperability Library and the `xeus` native implementation of the Jupyter protocol, xeus.\n", + "\n", + "- GitHub repository: https://github.com/compiler-research/xeus-cpp\n", + "- Documentation: https://xeus-cpp.readthedocs.io/en/latest/" + ] + }, + { + "cell_type": "markdown", + "id": "9af8aa44-7786-47b6-b94b-8579470acb3a", + "metadata": { + "papermill": { + "duration": 0.005621, + "end_time": "2025-10-30T18:09:12.388519", + "exception": false, + "start_time": "2025-10-30T18:09:12.382898", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## Usage\n", + "\n", + "
\n", + " \n", + " \n", + "
\n", + " To run the selected code cell, hit
Shift + Enter
\n", + "
\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "be1c8c6c-3fbe-4f53-9deb-8496c43d26ad", + "metadata": { + "papermill": { + "duration": 0.005724, + "end_time": "2025-10-30T18:09:12.400387", + "exception": false, + "start_time": "2025-10-30T18:09:12.394663", + "status": "completed" + }, + "tags": [] + }, + "source": [ + "## Output and error streams\n", + "\n", + "`std::cout` and `std::cerr` are redirected to the notebook frontend." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "9bd7f767-6c22-4a1b-a1e2-cd4184fd0367", + "metadata": { + "execution": { + "iopub.execute_input": "2025-10-30T18:09:12.410016Z", + "iopub.status.busy": "2025-10-30T18:09:12.409954Z", + "iopub.status.idle": "2025-10-30T18:09:13.332068Z", + "shell.execute_reply": "2025-10-30T18:09:13.332034Z" + }, + "papermill": { + "duration": 0.928161, + "end_time": "2025-10-30T18:09:13.333430", + "exception": false, + "start_time": "2025-10-30T18:09:12.405269", + "status": "completed" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "some output\n" + ] + } + ], + "source": [ + "#include \n", + "\n", + "std::cout << \"some output\" << std::endl;" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9622f20f-5925-4544-a97b-aada3a14209a", + "metadata": { + "execution": { + "iopub.execute_input": "2025-10-30T18:09:13.338627Z", + "iopub.status.busy": "2025-10-30T18:09:13.338586Z", + "iopub.status.idle": "2025-10-30T18:09:13.351259Z", + "shell.execute_reply": "2025-10-30T18:09:13.351198Z" + }, + "papermill": { + "duration": 0.01596, + "end_time": "2025-10-30T18:09:13.352040", + "exception": false, + "start_time": "2025-10-30T18:09:13.336080", + "status": "completed" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "std::cerr << \"some error\" << std::endl;" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "78383629-fd24-488a-be54-20248c34cb9f", + "metadata": { + "execution": { + "iopub.execute_input": "2025-10-30T18:09:13.357482Z", + "iopub.status.busy": "2025-10-30T18:09:13.357440Z", + "iopub.status.idle": "2025-10-30T18:09:13.358090Z", + "shell.execute_reply": "2025-10-30T18:09:13.358060Z" + }, + "papermill": { + "duration": 0.003817, + "end_time": "2025-10-30T18:09:13.358648", + "exception": false, + "start_time": "2025-10-30T18:09:13.354831", + "status": "completed" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include " + ] + }, + { + "cell_type": "markdown", + "id": "e99744a5", + "metadata": { + "tags": [ + "papermill-error-cell-tag" + ] + }, + "source": [ + "Execution using papermill encountered an exception here and stopped:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "945b7054-ed4d-4270-ae9f-c7696392a5d3", + "metadata": { + "execution": { + "iopub.execute_input": "2025-10-30T18:09:13.363616Z", + "iopub.status.busy": "2025-10-30T18:09:13.363585Z", + "iopub.status.idle": "2025-10-30T18:09:13.367272Z", + "shell.execute_reply": "2025-10-30T18:09:13.367226Z" + }, + "papermill": { + "duration": 0.007177, + "end_time": "2025-10-30T18:09:13.368304", + "exception": true, + "start_time": "2025-10-30T18:09:13.361127", + "status": "failed" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [ + { + "ename": "Standard Exception: ", + "evalue": "Unknown exception", + "output_type": "error", + "traceback": [ + "Standard Exception: Unknown exception" + ] + } + ], + "source": [ + "throw std::runtime_error(\"Unknown exception\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7af0c962-17dc-47d4-9772-b8a06e2bda3a", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "int j = 5;\n", + "std::cout << j << std::endl;" + ] + }, + { + "cell_type": "markdown", + "id": "526a7dba-4001-47d5-a116-95423118e100", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "# Interpreting the C++ programming language\n", + "\n", + "You can define functions, classes, templates, etc ..." + ] + }, + { + "cell_type": "markdown", + "id": "e5b116ce-ced1-4aa4-b14e-ef7d2606202e", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "## Functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86b08f22-e16c-4eac-917d-ae6eeb7ec7cb", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "double sqr(double a)\n", + "{\n", + " return a * a;\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5aff6711-54bf-4280-a496-c9f7c683eee5", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "double a = 2.5;\n", + "double asqr = sqr(a);\n", + "std::cout << asqr << std::endl;" + ] + }, + { + "cell_type": "markdown", + "id": "5b3959b0-9dc7-41a4-bba1-e20abd0765f7", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "## Classes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d981a53b-8185-49c5-8a30-02453cc1b9e9", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "class Foo\n", + "{\n", + "public:\n", + "\n", + " virtual ~Foo() {}\n", + " \n", + " virtual void print(double value) const\n", + " {\n", + " std::cout << \"Foo value = \" << value << std::endl;\n", + " }\n", + "};" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "195d513f-d5cb-4e3d-a6cb-ae99dfcd9aab", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "Foo bar;\n", + "bar.print(1.2);" + ] + }, + { + "cell_type": "markdown", + "id": "9ecc1588-cb6e-4676-bb16-b9938e980b06", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "## Polymorphism" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4df90bea-5c9e-462e-bd20-d80fd169b7b6", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "class Bar : public Foo\n", + "{\n", + "public:\n", + "\n", + " virtual ~Bar() {}\n", + " \n", + " virtual void print(double value) const\n", + " {\n", + " std::cout << \"Bar value = \" << 2 * value << std::endl;\n", + " }\n", + "};" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7dbbcc2-0f00-48eb-8bb9-94e871afa2a7", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "Foo* bar2 = new Bar;\n", + "bar2->print(1.2);\n", + "delete bar2;" + ] + }, + { + "cell_type": "markdown", + "id": "094f4ca7-0aa5-4121-bfff-bf5db1d42c5d", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "## Templates" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0df4f3a5-25a1-4548-ba63-54887c770dad", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include \n", + "\n", + "template \n", + "class FooT\n", + "{\n", + "public:\n", + " \n", + " explicit FooT(const T& t) : m_t(t) {}\n", + " \n", + " void print() const\n", + " {\n", + " std::cout << typeid(T).name() << \" m_t = \" << m_t << std::endl;\n", + " }\n", + " \n", + "private:\n", + " \n", + " T m_t;\n", + "};" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7bcab70-b9db-409c-aa04-9c64b413e266", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "FooT foot(1.2);\n", + "foot.print();" + ] + }, + { + "cell_type": "markdown", + "id": "9c61e669-5bc6-42de-bc9d-cc086d8d4779", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "## Documentation\n", + "\n", + " - Documentation for types of the standard library is retrieved on cppreference.com." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "978e592f-301b-4fea-9e12-5636d8b74ecd", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "?std::vector" + ] + }, + { + "cell_type": "markdown", + "id": "6c8561b1-23e7-425d-b333-8a47b507e764", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "## Using the `display_data` mechanism" + ] + }, + { + "cell_type": "markdown", + "id": "cb1ec203-98b0-40e5-bd78-31e6f40abe11", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "For a user-defined type `T`, the rich rendering in the notebook and JupyterLab can be enabled by by implementing the function `nl::json mime_bundle_repr(const T& im)`, which returns the JSON mime bundle for that type." + ] + }, + { + "cell_type": "markdown", + "id": "066fcd12-0f5e-4d8b-bf37-e55a41e85c57", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "### Image example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a5069c5-9d66-42b4-8de7-3337a6e53460", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include \n", + "#include \n", + "\n", + "#include \"nlohmann/json.hpp\"\n", + "\n", + "#include \"xeus/xbase64.hpp\"\n", + "\n", + "namespace nl = nlohmann;\n", + "\n", + "namespace im\n", + "{\n", + " struct image\n", + " { \n", + " inline image(const std::string& filename)\n", + " {\n", + " std::ifstream fin(filename, std::ios::binary); \n", + " m_buffer << fin.rdbuf();\n", + " }\n", + " \n", + " std::stringstream m_buffer;\n", + " };\n", + " \n", + " nl::json mime_bundle_repr(const image& i)\n", + " {\n", + " auto bundle = nl::json::object();\n", + " bundle[\"image/png\"] = xeus::base64encode(i.m_buffer.str());\n", + " return bundle;\n", + " }\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24c9b7e1-fbdb-4afc-8c13-ec0bcda595a9", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "im::image marie(\"images/marie.png\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de6ba06d-ed19-40b6-b31d-b8782fb81174", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include \"xcpp/xdisplay.hpp\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f08d536-1023-4f30-b0e3-9d6b5dfbf1be", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "xcpp::display(marie);" + ] + }, + { + "cell_type": "markdown", + "id": "387d6a7a-7ca4-41d6-9809-4040641db338", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "### Audio example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5aab5534-8420-4341-bf59-546d5f24bbed", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include \n", + "#include \n", + "\n", + "#include \"nlohmann/json.hpp\"\n", + "\n", + "#include \"xeus/xbase64.hpp\"\n", + "\n", + "namespace nl = nlohmann;\n", + "\n", + "namespace au\n", + "{\n", + " struct audio\n", + " { \n", + " inline audio(const std::string& filename)\n", + " {\n", + " std::ifstream fin(filename, std::ios::binary); \n", + " m_buffer << fin.rdbuf();\n", + " }\n", + " \n", + " std::stringstream m_buffer;\n", + " };\n", + " \n", + " nl::json mime_bundle_repr(const audio& a)\n", + " {\n", + " auto bundle = nl::json::object();\n", + " bundle[\"text/html\"] =\n", + " std::string(\"\";\n", + " return bundle;\n", + " }\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b9e8220-fe5a-498e-93a1-a93e71bda629", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "au::audio drums(\"audio/audio.wav\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "faee9bb7-22e4-49ad-8483-38cc1a044b19", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "xcpp::display(drums);" + ] + }, + { + "cell_type": "markdown", + "id": "d32274d3-61eb-4763-9b10-b3d4db09ae5c", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "### Update-display" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ba5126e-ea3a-4393-84a1-99fac70ae5e4", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include \n", + "#include \"xcpp/xdisplay.hpp\"\n", + "\n", + "#include \"nlohmann/json.hpp\"\n", + "\n", + "namespace nl = nlohmann;\n", + "\n", + "namespace ht\n", + "{\n", + " struct html\n", + " { \n", + " inline html(const std::string& content)\n", + " {\n", + " m_content = content;\n", + " }\n", + " std::string m_content;\n", + " };\n", + "\n", + " nl::json mime_bundle_repr(const html& a)\n", + " {\n", + " auto bundle = nl::json::object();\n", + " bundle[\"text/html\"] = a.m_content;\n", + " return bundle;\n", + " }\n", + "}\n", + "\n", + "// A blue rectangle\n", + "ht::html rect(R\"(\n", + "
\n", + "Original\n", + "
)\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3de3009-bf70-4adf-9471-900be0bc3d2d", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "xcpp::display(rect, \"some_display_id\");" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "629fa3d5-6c5d-40ad-937d-6257ae6c827d", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "// Update the rectangle to be red\n", + "rect.m_content = R\"(\n", + "
\n", + "Updated\n", + "
)\";\n", + "\n", + "xcpp::display(rect, \"some_display_id\", true);" + ] + }, + { + "cell_type": "markdown", + "id": "d4828c73-b061-4f8f-9966-f45d570c6567", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "source": [ + "### Clear output" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8391fa79-f365-4792-b1f6-b2f68d79a3d6", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "#include \n", + "#include \n", + "#include \n", + "\n", + "#include \"xcpp/xdisplay.hpp\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16ab8930-6639-4c8d-855f-58ec70a62c17", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "std::cout << \"hello\" << std::endl;\n", + "std::this_thread::sleep_for(std::chrono::seconds(1));\n", + "xcpp::clear_output(); // will flicker when replacing \"hello\" with \"goodbye\"\n", + "std::this_thread::sleep_for(std::chrono::seconds(1));\n", + "std::cout << \"goodbye\" << std::endl;" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "347331f2-98cf-4111-989c-7e34b3d0a309", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "outputs": [], + "source": [ + "std::cout << \"hello\" << std::endl;\n", + "std::this_thread::sleep_for(std::chrono::seconds(1));\n", + "xcpp::clear_output(true); // prevents flickering\n", + "std::this_thread::sleep_for(std::chrono::seconds(1));\n", + "std::cout << \"goodbye\" << std::endl;" + ] + }, + { + "cell_type": "markdown", + "id": "92b12afd-2dbd-41df-acd0-25171c6dbb0d", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [], + "vscode": { + "languageId": "c++" + } + }, + "source": [ + "# Taking input from the user" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e9a85de0-df46-4850-b88a-4b752f29c1a4", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "std::string name;\n", + "std::cin >> name;" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f95eece8-cd43-443e-8e27-9b901afefb20", + "metadata": { + "papermill": { + "duration": null, + "end_time": null, + "exception": null, + "start_time": null, + "status": "pending" + }, + "tags": [] + }, + "outputs": [], + "source": [ + "std::cout << \"Your name is\" << name;" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++20", + "language": "cpp", + "name": "xcpp20" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "C++", + "version": "" + }, + "papermill": { + "default_parameters": {}, + "duration": 1.526501, + "end_time": "2025-10-30T18:09:13.485553", + "environment_variables": {}, + "exception": true, + "input_path": "Notebooks/xeus-cpp.ipynb", + "output_path": "Notebooks/xeus-cpp_output.ipynb", + "parameters": {}, + "start_time": "2025-10-30T18:09:11.959052", + "version": "2.6.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f2c9d998..b34fe468 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -46,9 +46,6 @@ find_package(doctest) set(XEUS_CPP_TESTS main.cpp test_interpreter.cpp - test_debugger.cpp - xeus_client.hpp - xeus_client.cpp ) add_executable(test_xeus_cpp ${XEUS_CPP_TESTS}) diff --git a/test/debugger/__init__.py b/test/debugger/__init__.py deleted file mode 100644 index 5619d37b..00000000 --- a/test/debugger/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test package for xeus-cpp debugger""" diff --git a/test/debugger/temp.json b/test/debugger/temp.json deleted file mode 100644 index f6610cad..00000000 --- a/test/debugger/temp.json +++ /dev/null @@ -1,16 +0,0 @@ -{'header': {'date': datetime.datetime(2025, - 10, - 18, - 14, - 27, - 51, - 315667, tzinfo=tzutc()), 'msg_id': 'dab29c56c1564118969e17dd57368818', 'msg_type': 'kernel_info_reply', 'session': '8fc3e1ee796e4bc09e668f57f6c1275e', 'username': 'abhinav', 'version': '5.3' - }, 'msg_id': 'dab29c56c1564118969e17dd57368818', 'msg_type': 'kernel_info_reply', 'parent_header': {'date': datetime.datetime(2025, - 10, - 18, - 14, - 27, - 51, - 315437, tzinfo=tzutc()), 'msg_id': '875bed24-938f5cb8f579d7c5e04515f7_29098_3', 'msg_type': 'kernel_info_request', 'session': '875bed24-938f5cb8f579d7c5e04515f7', 'username': 'abhinav', 'version': '5.3' - }, 'metadata': {}, 'content': {'banner': ' __ _____ _ _ ___\n \\ \\/ / _ \\ | | / __|\n > < __/ |_| \\__ \\\n /_/\\_\\___|\\__,_|___/\n\n xeus-cpp: a C++ Jupyter kernel - based on Clang-repl\n', 'debugger': True, 'help_links': [ - {'text': 'Xeus-cpp Reference', 'url': 'https: //xeus-cpp.readthedocs.io'}], 'implementation': 'xeus-cpp', 'implementation_version': '0.7.2', 'language_info': {'codemirror_mode': 'text/x-c++src', 'file_extension': '.cpp', 'mimetype': 'text/x-c++src', 'name': 'C++', 'version': ''}, 'protocol_version': '5.3', 'status': 'ok'}, 'buffers': []} \ No newline at end of file diff --git a/test/debugger/test_debugger.py b/test/debugger/test_debugger.py deleted file mode 100644 index fa2c86c3..00000000 --- a/test/debugger/test_debugger.py +++ /dev/null @@ -1,336 +0,0 @@ -"""Test xeus-cpp debugger functionality using LLDB""" - -import sys -import pytest -import time -from queue import Empty - -from util import TIMEOUT, get_replies, get_reply, new_kernel - -seq = 0 - -# xeus-cpp uses LLDB for debugging, not debugpy -# Test if xeus-cpp kernel with debugger support is available -try: - from jupyter_client import kernelspec - ksm = kernelspec.KernelSpecManager() - available_kernels = ksm.find_kernel_specs() - print(f"Available kernels: {available_kernels}") - HAS_XCPP_DEBUGGER = any('xcpp17-debugger' in k for k in available_kernels.keys()) - print(f"xeus-cpp debugger available: {HAS_XCPP_DEBUGGER}") -except Exception: - HAS_XCPP_DEBUGGER = False - - -def send_debug_request(kernel, command, arguments=None, timeout=TIMEOUT): - """Send a debug request and get reply""" - global seq - seq += 1 - - msg = kernel.session.msg( - "debug_request", - { - "type": "request", - "seq": seq, - "command": command, - "arguments": arguments or {}, - }, - ) - - kernel.control_channel.send(msg) - reply = get_reply(kernel, msg["header"]["msg_id"], channel="control", timeout=timeout) - return reply.get("content", {}) - -@pytest.fixture() -def kernel(): - """Create a fresh xeus-cpp kernel""" - with new_kernel(kernel_name="xcpp17-debugger") as kc: - # Give kernel time to fully start - time.sleep(1) - yield kc - - -def test_kernel_starts(): - """Test that kernel can start""" - with new_kernel(kernel_name="xcpp17-debugger") as kc: - # Execute simple code - msg_id = kc.execute("int test = 1;") - reply = kc.get_shell_msg(timeout=TIMEOUT) - assert reply["content"]["status"] == "ok" - print("✓ Kernel starts and executes code") - - -# def test_debug_initialize(): -# """Test debugger initialization""" -# with new_kernel(kernel_name="xcpp17-debugger") as kc: -# # Execute some code first to ensure interpreter is ready -# kc.execute("int x = 42;") -# time.sleep(0.5) - -# reply = send_debug_request( -# kc, -# "initialize", -# { -# "clientID": "test-client", -# "clientName": "testClient", -# "adapterID": "lldb", -# "pathFormat": "path", -# "linesStartAt1": True, -# "columnsStartAt1": True, -# "supportsVariableType": True, -# "supportsVariablePaging": False, -# "supportsRunInTerminalRequest": False, -# "locale": "en", -# }, -# ) - -# print(f"Initialize reply: {reply}") - -# # Check we got a response -# assert isinstance(reply, dict) -# assert "type" in reply or "success" in reply - -# if reply.get("success"): -# print("✓ Debugger initialized successfully") -# else: -# print(f"⚠ Initialize returned: {reply}") - - -# def test_debug_attach_with_launch(): -# """Test debugger attachment using launch method""" -# with new_kernel(kernel_name="xcpp17-debugger") as kc: -# # Execute code to ensure process is active -# kc.execute("#include ") -# time.sleep(0.5) - -# # Initialize -# init_reply = send_debug_request( -# kc, -# "initialize", -# { -# "clientID": "test-client", -# "adapterID": "lldb", -# "pathFormat": "path", -# "linesStartAt1": True, -# "columnsStartAt1": True, -# }, -# ) - -# print(f"Initialize: {init_reply.get('success', False)}") - -# if not init_reply.get("success"): -# pytest.skip(f"Initialize failed: {init_reply}") - -# # Try launch instead of attach -# launch_reply = send_debug_request( -# kc, -# "launch", -# { -# "stopOnEntry": False, -# "justMyCode": False, -# } -# ) - -# print(f"Launch reply: {launch_reply}") - -# if launch_reply.get("success"): -# print("✓ Debugger launched successfully") -# else: -# # Try attach as fallback -# attach_reply = send_debug_request( -# kc, -# "attach", -# { -# "stopOnEntry": False, -# } -# ) -# print(f"Attach reply: {attach_reply}") - -# if not attach_reply.get("success"): -# error = attach_reply.get("body", {}).get("error", {}) -# pytest.skip(f"Could not attach: {error.get('format', attach_reply)}") - - -# def test_dump_cell(): -# """Test dumping cell code""" -# with new_kernel(kernel_name="xcpp17-debugger") as kc: -# # Initialize debugger first -# kc.execute("int dummy = 0;") -# time.sleep(0.5) - -# init_reply = send_debug_request( -# kc, -# "initialize", -# { -# "clientID": "test-client", -# "clientName": "testClient", -# "adapterID": "lldb", -# "pathFormat": "path", -# "linesStartAt1": True, -# "columnsStartAt1": True, -# "supportsVariableType": True, -# "supportsVariablePaging": False, -# "supportsRunInTerminalRequest": False, -# "locale": "en", -# }, -# ) - -# print(f"Initialize reply: {init_reply}") - -# if not init_reply.get("success"): -# pytest.skip("Initialize failed") - -# # Dump a cell -# code = """#include -# int add(int a, int b) { -# return a + b; -# } -# int result = add(2, 3);""" - -# dump_reply = send_debug_request( -# kc, -# "dumpCell", -# {"code": code} -# ) - -# # print(f"DumpCell reply: {dump_reply}") - -# if dump_reply.get("success"): -# source_path = dump_reply.get("body", {}).get("sourcePath") -# print(f"✓ Cell dumped to: {source_path}") -# assert source_path is not None -# else: -# print(f"⚠ DumpCell failed: {dump_reply}") - - -# def test_configuration_done(): -# """Test configuration done message""" -# with new_kernel(kernel_name="xcpp17-debugger") as kc: -# kc.execute("int x = 1;") -# time.sleep(0.5) - -# # Initialize -# init_reply = send_debug_request(kc, "initialize", -# { -# "clientID": "test-client", -# "clientName": "testClient", -# "adapterID": "lldb", -# "pathFormat": "path", -# "linesStartAt1": True, -# "columnsStartAt1": True, -# "supportsVariableType": True, -# "supportsVariablePaging": False, -# "supportsRunInTerminalRequest": False, -# "locale": "en", -# }) - -# if not init_reply.get("success"): -# pytest.skip("Initialize failed") - -# # Launch -# launch_reply = send_debug_request(kc, "attach", { -# "stopOnEntry": False, -# }) - -# if not launch_reply.get("success"): -# pytest.skip("Launch failed") - -# # Configuration done -# config_reply = send_debug_request(kc, "configurationDone") - -# print(f"ConfigurationDone reply: {config_reply}") - -# assert isinstance(config_reply, dict) -# print("✓ Configuration done sent") - - -def test_breakpoint_without_execution(): - """Test setting breakpoint (without execution)""" - with new_kernel(kernel_name="xcpp17-debugger") as kc: - kc.execute("int x = 1;") - time.sleep(0.5) - - # Initialize - init_reply = send_debug_request(kc, "initialize", - { - "clientID": "test-client", - "clientName": "testClient", - "adapterID": "lldb", - "pathFormat": "path", - "linesStartAt1": True, - "columnsStartAt1": True, - "supportsVariableType": True, - "supportsVariablePaging": False, - "supportsRunInTerminalRequest": False, - "locale": "en", - }) - - if not init_reply.get("success"): - pytest.skip("Initialize failed") - - # Launch - launch_reply = send_debug_request(kc, "attach", { - "stopOnEntry": False, - }) - - if not launch_reply.get("success"): - pytest.skip("Attach failed") - - # Dump cell - code = """int multiply(int a, int b) { - int result = a * b; - return result; -}""" - # execute code to have something in the interpreter - kc.execute(code) - time.sleep(0.5) - - dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) - - if not dump_reply.get("success"): - pytest.skip("DumpCell failed") - - source = dump_reply["body"]["sourcePath"] - - print(f"Dumped source path: {source}") - - # Set breakpoint - bp_reply = send_debug_request( - kc, - "setBreakpoints", - { - "source": {"path": source}, - "breakpoints": [{"line": 2}], - "sourceModified": False, - } - ) - - print(f"SetBreakpoints reply: {bp_reply}") - - if bp_reply.get("success"): - breakpoints = bp_reply.get("body", {}).get("breakpoints", []) - print(f"✓ Set {len(breakpoints)} breakpoint(s)") - if breakpoints: - print(f" Verified: {breakpoints[0].get('verified', False)}") - else: - print(f"⚠ SetBreakpoints failed: {bp_reply}") - - -# @pytest.mark.skipif(not HAS_XCPP, reason="xeus-cpp kernel not available") -# def test_kernel_info_debugger_support(): -# """Check if kernel advertises debugger support""" -# with new_kernel(kernel_name="xcpp17") as kc: -# kc.kernel_info() -# reply = kc.get_shell_msg(timeout=TIMEOUT) - -# features = reply["content"].get("supported_features", []) -# print(f"Supported features: {features}") - -# if "debugger" in features: -# print("✓ Kernel advertises debugger support") -# else: -# print("⚠ Debugger not in supported features (may still work)") - - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) \ No newline at end of file diff --git a/test/debugger/util.py b/test/debugger/util.py deleted file mode 100644 index d424cf33..00000000 --- a/test/debugger/util.py +++ /dev/null @@ -1,332 +0,0 @@ -"""utilities for testing xeus-cpp kernels""" - -from __future__ import annotations - -import atexit -import os -import sys -from contextlib import contextmanager -from queue import Empty -from subprocess import STDOUT -from tempfile import TemporaryDirectory -from time import time - -from jupyter_client import manager -from jupyter_client.blocking.client import BlockingKernelClient - -STARTUP_TIMEOUT = 60 -TIMEOUT = 100 - -KM: manager.KernelManager = None # type:ignore -KC: BlockingKernelClient = None # type:ignore - - -def start_new_kernel(**kwargs): - """start a new xeus-cpp kernel, and return its Manager and Client - - Integrates with our output capturing for tests. - """ - kwargs["stderr"] = STDOUT - # Specify xeus-cpp kernel - kwargs.setdefault("kernel_name", "xcpp17-debugger") - return manager.start_new_kernel(startup_timeout=STARTUP_TIMEOUT, **kwargs) - - -def flush_channels(kc=None): - """flush any messages waiting on the queue""" - if kc is None: - kc = KC - for get_msg in (kc.get_shell_msg, kc.get_iopub_msg): - while True: - try: - msg = get_msg(timeout=0.1) - except Empty: - break - - -def get_reply(kc, msg_id, timeout=TIMEOUT, channel="shell"): - """Get a reply for a specific message ID""" - t0 = time() - while True: - get_msg = getattr(kc, f"get_{channel}_msg") - reply = get_msg(timeout=timeout) - if reply["parent_header"]["msg_id"] == msg_id: - break - # Allow debugging ignored replies - print(f"Ignoring reply not to {msg_id}: {reply}") - t1 = time() - timeout -= t1 - t0 - t0 = t1 - return reply - - -def get_replies(kc, msg_ids: list[str], timeout=TIMEOUT, channel="shell"): - """Get replies which may arrive in any order""" - count = 0 - replies = [None] * len(msg_ids) - while count < len(msg_ids): - get_msg = getattr(kc, f"get_{channel}_msg") - reply = get_msg(timeout=timeout) - try: - msg_id = reply["parent_header"]["msg_id"] - replies[msg_ids.index(msg_id)] = reply - count += 1 - except ValueError: - print(f"Ignoring reply not to any of {msg_ids}: {reply}") - return replies - - -def execute(code="", kc=None, **kwargs): - """Execute C++ code and validate the execution request - - Args: - code: C++ code to execute - kc: kernel client (uses global KC if None) - **kwargs: additional arguments for kc.execute() - - Returns: - tuple: (msg_id, reply_content) - """ - if kc is None: - kc = KC - msg_id = kc.execute(code=code, **kwargs) - reply = get_reply(kc, msg_id, TIMEOUT) - - # Validate it's an execute_reply - assert reply["msg_type"] == "execute_reply", f"Expected execute_reply, got {reply['msg_type']}" - - busy = kc.get_iopub_msg(timeout=TIMEOUT) - assert busy["msg_type"] == "status" - assert busy["content"]["execution_state"] == "busy" - - if not kwargs.get("silent"): - execute_input = kc.get_iopub_msg(timeout=TIMEOUT) - assert execute_input["msg_type"] == "execute_input" - assert execute_input["content"]["code"] == code - - # show tracebacks/errors if present for debugging - if reply["content"].get("traceback"): - print("\n".join(reply["content"]["traceback"]), file=sys.stderr) - - if reply["content"]["status"] == "error": - print(f"Error: {reply['content'].get('ename', 'Unknown')}", file=sys.stderr) - print(f"Message: {reply['content'].get('evalue', '')}", file=sys.stderr) - - return msg_id, reply["content"] - - -def start_global_kernel(kernel_name="xcpp17"): - """start the global kernel (if it isn't running) and return its client - - Args: - kernel_name: xeus-cpp kernel variant (xcpp11, xcpp14, xcpp17, xcpp20) - """ - global KM, KC - if KM is None: - KM, KC = start_new_kernel(kernel_name=kernel_name) - atexit.register(stop_global_kernel) - else: - flush_channels(KC) - return KC - - -@contextmanager -def kernel(kernel_name="xcpp17"): - """Context manager for the global kernel instance - - Should be used for most kernel tests - - Args: - kernel_name: xeus-cpp kernel variant (xcpp11, xcpp14, xcpp17, xcpp20) - - Yields: - kernel_client: connected KernelClient instance - """ - yield start_global_kernel(kernel_name=kernel_name) - - -def uses_kernel(test_f): - """Decorator for tests that use the global kernel""" - - def wrapped_test(): - with kernel() as kc: - test_f(kc) - - wrapped_test.__doc__ = test_f.__doc__ - wrapped_test.__name__ = test_f.__name__ - return wrapped_test - - -def stop_global_kernel(): - """Stop the global shared kernel instance, if it exists""" - global KM, KC - if KC is not None: - KC.stop_channels() - KC = None # type:ignore - if KM is not None: - KM.shutdown_kernel(now=True) - KM = None # type:ignore - - -@contextmanager -def new_kernel(argv=None, kernel_name="xcpp17"): - """Context manager for a new kernel in a subprocess - - Should only be used for tests where the kernel must not be reused. - - Args: - argv: additional command-line arguments - kernel_name: xeus-cpp kernel variant (xcpp11, xcpp14, xcpp17, xcpp20) - - Yields: - kernel_client: connected KernelClient instance - """ - kwargs = {"stderr": STDOUT, "kernel_name": kernel_name} - if argv is not None: - kwargs["extra_arguments"] = argv - with manager.run_kernel(**kwargs) as kc: - yield kc - - -def assemble_output(get_msg, timeout=1, parent_msg_id: str | None = None): - """Assemble stdout/stderr from an execution - - Args: - get_msg: function to get messages (usually kc.get_iopub_msg) - timeout: timeout for getting messages - parent_msg_id: filter messages by parent message ID - - Returns: - tuple: (stdout, stderr) - """ - stdout = "" - stderr = "" - while True: - try: - msg = get_msg(timeout=timeout) - except Empty: - break - - msg_type = msg["msg_type"] - content = msg["content"] - - if parent_msg_id is not None and msg["parent_header"]["msg_id"] != parent_msg_id: - continue - - if msg_type == "status" and content["execution_state"] == "idle": - break - elif msg_type == "stream": - if content["name"] == "stdout": - stdout += content["text"] - elif content["name"] == "stderr": - stderr += content["text"] - else: - raise KeyError("bad stream: %r" % content["name"]) - - return stdout, stderr - - -def wait_for_idle(kc, parent_msg_id: str | None = None, timeout=TIMEOUT): - """Wait for kernel to become idle - - Args: - kc: kernel client - parent_msg_id: optional parent message ID to match - timeout: maximum time to wait - """ - start_time = time() - while True: - elapsed = time() - start_time - if elapsed > timeout: - raise TimeoutError(f"Kernel did not become idle within {timeout}s") - - msg = kc.get_iopub_msg(timeout=min(1, timeout - elapsed)) - msg_type = msg["msg_type"] - content = msg["content"] - if ( - msg_type == "status" - and content["execution_state"] == "idle" - and (parent_msg_id is None or msg["parent_header"]["msg_id"] == parent_msg_id) - ): - break - - -class TemporaryWorkingDirectory(TemporaryDirectory): - """ - Creates a temporary directory and sets the cwd to that directory. - Automatically reverts to previous cwd upon cleanup. - - Usage example: - with TemporaryWorkingDirectory() as tmpdir: - ... - """ - - def __enter__(self): - self.old_wd = os.getcwd() - os.chdir(self.name) - return super().__enter__() - - def __exit__(self, exc, value, tb): - os.chdir(self.old_wd) - return super().__exit__(exc, value, tb) - - -# C++-specific helper functions - -def compile_and_run(code: str, kc=None, includes: list[str] = None, - flags: list[str] = None) -> tuple[str, dict]: - """Helper to compile and run C++ code with common setup - - Args: - code: C++ code to execute - kc: kernel client - includes: list of headers to include - flags: compiler flags to set - - Returns: - tuple: (msg_id, reply_content) - """ - if kc is None: - kc = KC - - full_code = "" - - # Add includes - if includes: - for inc in includes: - full_code += f"#include <{inc}>\n" - full_code += "\n" - - # Add compiler flags if needed - if flags: - for flag in flags: - full_code += f"#pragma cling add_compiler_flag {flag}\n" - full_code += "\n" - - full_code += code - - return execute(full_code, kc) - - -def check_cpp_version(kc=None) -> str: - """Check which C++ standard is being used""" - if kc is None: - kc = KC - - code = """ - #include - std::cout << __cplusplus << std::endl; - """ - - msg_id, reply = execute(code, kc) - stdout, _ = assemble_output(kc.get_iopub_msg, parent_msg_id=msg_id) - - version_map = { - "199711": "C++98", - "201103": "C++11", - "201402": "C++14", - "201703": "C++17", - "202002": "C++20", - } - - return version_map.get(stdout.strip(), f"Unknown ({stdout.strip()})") \ No newline at end of file diff --git a/test/test_debugger.py b/test/test_debugger.py new file mode 100644 index 00000000..ebfc78ca --- /dev/null +++ b/test/test_debugger.py @@ -0,0 +1,512 @@ +"""Test xeus-cpp debugger functionality using LLDB""" + +import pytest +import time +from queue import Empty +from subprocess import STDOUT +from jupyter_client import manager +from contextlib import contextmanager +from time import time + +KM: manager.KernelManager = None # type:ignore + +seq = 0 +TIMEOUT=100 +KERNEL_NAME = "xcpp17-debugger" + +def get_reply(kc, msg_id, timeout=TIMEOUT, channel="shell"): + t0 = time() + while True: + get_msg = getattr(kc, f"get_{channel}_msg") + reply = get_msg(timeout=timeout) + if reply["parent_header"]["msg_id"] == msg_id: + break + # Allow debugging ignored replies + print(f"Ignoring reply not to {msg_id}: {reply}") + t1 = time() + timeout -= t1 - t0 + t0 = t1 + return reply + +@contextmanager +def new_kernel(argv=None, kernel_name=KERNEL_NAME): + """Context manager for a new kernel in a subprocess + + Should only be used for tests where the kernel must not be reused. + + Args: + argv: additional command-line arguments + kernel_name: xeus-cpp kernel variant (xcpp11, xcpp14, xcpp17, xcpp20) + + Yields: + kernel_client: connected KernelClient instance + """ + kwargs = {"stderr": STDOUT, "kernel_name": kernel_name} + if argv is not None: + kwargs["extra_arguments"] = argv + with manager.run_kernel(**kwargs) as kc: + yield kc + +# xeus-cpp uses LLDB for debugging +# Test if xeus-cpp kernel with debugger support is available +try: + from jupyter_client import kernelspec + ksm = kernelspec.KernelSpecManager() + available_kernels = ksm.find_kernel_specs() + print(f"Available kernels: {available_kernels}") + HAS_XCPP_DEBUGGER = any(KERNEL_NAME in k for k in available_kernels.keys()) + print(f"xeus-cpp debugger available: {HAS_XCPP_DEBUGGER}") +except Exception: + HAS_XCPP_DEBUGGER = False + + +def send_debug_request(kernel, command, arguments=None, timeout=TIMEOUT): + """Send a debug request and get reply""" + global seq + seq += 1 + + msg = kernel.session.msg( + "debug_request", + { + "type": "request", + "seq": seq, + "command": command, + "arguments": arguments or {}, + }, + ) + + kernel.control_channel.send(msg) + reply = get_reply(kernel, msg["header"]["msg_id"], channel="control", timeout=timeout) + return reply.get("content", {}) + +def send_initialize_request(kc): + """Send Initialize request""" + + reply = send_debug_request( + kc, + "initialize", + { + "clientID": "test-client", + "clientName": "testClient", + "adapterID": "lldb", + "pathFormat": "path", + "linesStartAt1": True, + "columnsStartAt1": True, + "supportsVariableType": True, + "supportsVariablePaging": False, + "supportsRunInTerminalRequest": False, + "locale": "en", + }, + ) + + return reply + + +@pytest.fixture() +def kernel(): + """Create a fresh xeus-cpp kernel""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + # Give kernel time to fully start + time.sleep(1) + yield kc + + +def test_kernel_starts(): + """Test that kernel can start""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + msg_id = kc.execute("int test = 1;") + reply = kc.get_shell_msg(timeout=TIMEOUT) + assert reply["content"]["status"] == "ok" + +def test_debug_initialize(): + """Test debugger initialization""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + reply = send_initialize_request(kc) + assert reply["success"] == True + + +def test_debug_attach(): + """Test debugger attachment using attach method""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + +def test_dump_cell(): + """Test dumping cell code""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +#include +int add(int a, int b) { + return a + b; +} +int result = add(2, 3); +""" + dump_reply = send_debug_request( + kc, + "dumpCell", + {"code": code} + ) + assert dump_reply["success"] == True + + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + +def test_set_breakpoints(): + """Test setting breakpoint (without execution)""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +#include +int add(int a, int b) { + return a + b; +} +int result = add(2, 3); +""" + kc.execute(code) + + dump_reply = send_debug_request( + kc, + "dumpCell", + {"code": code} + ) + assert dump_reply["success"] == True + + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 2}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + +def test_stop_on_breakpoint(): + """Test setting breakpoint and hitting it during execution""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +int divide(int a, int b) { + int result = a / b; + return result; +} +divide(10, 2); + """ + + kc.execute(code) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 2}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + + msg_id = kc.execute("divide(10, 2);") + + msg: dict = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "breakpoint" + +def test_step_in(): + """Test stepping into a function""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +int multiply(int a, int b) { + return a * b; +} +int calculate(int x, int y) { + int product = multiply(x, y); + return product; +} +calculate(5, 3); +""" + kc.execute(code) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 5}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + + msg_id = kc.execute("calculate(5, 3);") + + msg: dict = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "breakpoint" + + threadId = int(msg["content"]["body"]["threadId"]) + + step_in_reply = send_debug_request(kc, "stepIn", {"threadId": threadId}) + assert step_in_reply["success"] == True + + msg = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "step" + +def test_step_out(): + """Test stepping out of a function""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +int subtract(int a, int b) { + int result = a - b; + return result; +} +int compute(int x, int y) { + int difference = subtract(x, y); + return difference; +} +compute(10, 4); +""" + kc.execute(code) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + # Set breakpoint inside subtract function + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 2}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + + msg_id = kc.execute("compute(10, 4);") + + msg: dict = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "breakpoint" + + threadId = int(msg["content"]["body"]["threadId"]) + + step_out_reply = send_debug_request(kc, "stepOut", {"threadId": threadId}) + assert step_out_reply["success"] == True + + msg = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "step" + +def test_continue_execution(): + """Test continuing execution after hitting breakpoint""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +int factorial(int n) { + if (n <= 1) return 1; + return n * factorial(n - 1); +} +int result = factorial(5); +""" + kc.execute(code) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 2}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + + msg_id = kc.execute("factorial(5);") + + # Wait for breakpoint hit + msg: dict = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "breakpoint" + + threadId = int(msg["content"]["body"]["threadId"]) + + continue_reply = send_debug_request(kc, "continue", {"threadId": threadId}) + assert continue_reply["success"] == True + +def test_stack_trace(): + """Test getting stack trace at breakpoint""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +int inner(int x) { + return x * 2; +} +int outer(int y) { + return inner(y + 1); +} +outer(5); +""" + kc.execute(code) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 2}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + + msg_id = kc.execute("outer(5);") + + msg: dict = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "breakpoint" + + threadId = int(msg["content"]["body"]["threadId"]) + + stacktrace_reply = send_debug_request(kc, "stackTrace", {"threadId": threadId}) + assert stacktrace_reply["success"] == True + assert len(stacktrace_reply.get("body", {}).get("stackFrames", [])) > 0 + +def test_inspect_variables(): + """Test inspecting variables at breakpoint""" + with new_kernel(kernel_name=KERNEL_NAME) as kc: + init_reply = send_initialize_request(kc) + assert init_reply["success"] == True + + attach_reply = send_debug_request(kc, "attach") + assert attach_reply["success"] == True + + code = """ +int process(int a, int b) { + int sum = a + b; + int product = a * b; + return sum + product; +} +process(3, 4); +""" + kc.execute(code) + + dump_reply = send_debug_request(kc, "dumpCell", {"code": code}) + source_path = dump_reply.get("body", {}).get("sourcePath") + assert source_path is not None + + bp_reply = send_debug_request( + kc, + "setBreakpoints", + { + "source": {"path": source_path}, + "breakpoints": [{"line": 3}], + "sourceModified": False, + } + ) + assert bp_reply["success"] == True + + msg_id = kc.execute("process(3, 4);") + + # Wait for breakpoint hit + msg: dict = {"msg_type": "", "content": {}} + while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped": + msg = kc.get_iopub_msg(timeout=TIMEOUT) + + assert msg["content"]["body"]["reason"] == "breakpoint" + + threadId = int(msg["content"]["body"]["threadId"]) + + # Get scopes + stacktrace_reply = send_debug_request(kc, "stackTrace", {"threadId": threadId}) + frame_id = stacktrace_reply["body"]["stackFrames"][0]["id"] + + scopes_reply = send_debug_request(kc, "scopes", {"frameId": frame_id}) + assert scopes_reply["success"] == True + + # Get variables + if scopes_reply["body"]["scopes"]: + var_ref = scopes_reply["body"]["scopes"][0]["variablesReference"] + vars_reply = send_debug_request(kc, "variables", {"variablesReference": var_ref}) + assert vars_reply["success"] == True + +if __name__ == "__main__": + pytest.main([__file__, "-v", "-s"])