diff --git a/src/mono/mono/mini/mini-wasm-debugger.c b/src/mono/mono/mini/mini-wasm-debugger.c index baef84e824cb60..c4f8031f959711 100644 --- a/src/mono/mono/mini/mini-wasm-debugger.c +++ b/src/mono/mono/mini/mini-wasm-debugger.c @@ -30,6 +30,12 @@ enum { EXCEPTION_MODE_ALL }; +// Flags for get_*_properties +#define GPFLAG_NONE 0x0000 +#define GPFLAG_OWN_PROPERTIES 0x0001 +#define GPFLAG_ACCESSORS_ONLY 0x0002 +#define GPFLAG_EXPAND_VALUETYPES 0x0004 + //functions exported to be used by JS G_BEGIN_DECLS @@ -41,14 +47,14 @@ EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_local_vars (int scope, int* pos, int EMSCRIPTEN_KEEPALIVE void mono_wasm_clear_all_breakpoints (void); EMSCRIPTEN_KEEPALIVE int mono_wasm_setup_single_step (int kind); EMSCRIPTEN_KEEPALIVE int mono_wasm_pause_on_exceptions (int state); -EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_object_properties (int object_id, gboolean expand_value_types); -EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_array_values (int object_id, int start_idx, int count, gboolean expand_value_types); +EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_object_properties (int object_id, int gpflags); +EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_array_values (int object_id, int start_idx, int count, int gpflags); EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_invoke_getter_on_object (int object_id, const char* name); EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_invoke_getter_on_value (void *value, MonoClass *klass, const char *name); EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_deref_ptr_value (void *value_addr, MonoClass *klass); //JS functions imported that we use -extern void mono_wasm_add_frame (int il_offset, int method_token, const char *assembly_name, const char *method_name); +extern void mono_wasm_add_frame (int il_offset, int method_token, int frame_id, const char *assembly_name, const char *method_name); extern void mono_wasm_fire_bp (void); extern void mono_wasm_fire_exception (int exception_obj_id, const char* message, const char* class_name, gboolean uncaught); extern void mono_wasm_add_obj_var (const char*, const char*, guint64); @@ -61,7 +67,7 @@ extern void mono_wasm_add_typed_value (const char *type, const char *str_value, G_END_DECLS -static void describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAsyncLocalThis, gboolean expandValueType); +static void describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAsyncLocalThis, int gpflags); static void handle_exception (MonoException *exc, MonoContext *throw_ctx, MonoContext *catch_ctx, StackFrameInfo *catch_frame); //FIXME move all of those fields to the profiler object @@ -137,10 +143,10 @@ collect_frames (MonoStackFrameInfo *info, MonoContext *ctx, gpointer data) if (!method) return FALSE; - DEBUG_PRINTF (2, "Reporting method %s native_offset %d\n", method->name, info->native_offset); + DEBUG_PRINTF (2, "collect_frames: Reporting method %s native_offset %d, wrapper_type: %d\n", method->name, info->native_offset, method->wrapper_type); if (!mono_find_prev_seq_point_for_native_offset (mono_get_root_domain (), method, info->native_offset, NULL, &sp)) - DEBUG_PRINTF (2, "Failed to lookup sequence point\n"); + DEBUG_PRINTF (2, "collect_frames: Failed to lookup sequence point. method: %s, native_offset: %d \n", method->name, info->native_offset); DbgEngineStackFrame *frame = g_new0 (DbgEngineStackFrame, 1); @@ -651,23 +657,25 @@ list_frames (MonoStackFrameInfo *info, MonoContext *ctx, gpointer data) MonoMethod *method; char *method_full_name; + int* frame_id_p = (int*)data; + (*frame_id_p)++; + //skip wrappers if (info->type != FRAME_TYPE_MANAGED && info->type != FRAME_TYPE_INTERP) return FALSE; - if (info->ji) method = jinfo_get_method (info->ji); else method = info->method; - if (!method) + if (!method || method->wrapper_type != MONO_WRAPPER_NONE) return FALSE; - DEBUG_PRINTF (2, "Reporting method %s native_offset %d\n", method->name, info->native_offset); + DEBUG_PRINTF (2, "list_frames: Reporting method %s native_offset %d, wrapper_type: %d\n", method->name, info->native_offset, method->wrapper_type); if (!mono_find_prev_seq_point_for_native_offset (mono_get_root_domain (), method, info->native_offset, NULL, &sp)) - DEBUG_PRINTF (1, "Failed to lookup sequence point\n"); + DEBUG_PRINTF (2, "list_frames: Failed to lookup sequence point. method: %s, native_offset: %d\n", method->name, info->native_offset); method_full_name = mono_method_full_name (method, FALSE); while (method->is_inflated) @@ -676,10 +684,8 @@ list_frames (MonoStackFrameInfo *info, MonoContext *ctx, gpointer data) char *assembly_name = g_strdup (m_class_get_image (method->klass)->module_name); inplace_tolower (assembly_name); - if (method->wrapper_type == MONO_WRAPPER_NONE) { - DEBUG_PRINTF (2, "adding off %d token %d assembly name %s\n", sp.il_offset, mono_metadata_token_index (method->token), assembly_name); - mono_wasm_add_frame (sp.il_offset, mono_metadata_token_index (method->token), assembly_name, method_full_name); - } + DEBUG_PRINTF (2, "adding off %d token %d assembly name %s\n", sp.il_offset, mono_metadata_token_index (method->token), assembly_name); + mono_wasm_add_frame (sp.il_offset, mono_metadata_token_index (method->token), *frame_id_p, assembly_name, method_full_name); g_free (assembly_name); @@ -689,7 +695,8 @@ list_frames (MonoStackFrameInfo *info, MonoContext *ctx, gpointer data) EMSCRIPTEN_KEEPALIVE void mono_wasm_enum_frames (void) { - mono_walk_stack_with_ctx (list_frames, NULL, MONO_UNWIND_NONE, NULL); + int frame_id = -1; + mono_walk_stack_with_ctx (list_frames, NULL, MONO_UNWIND_NONE, &frame_id); } static char* @@ -763,6 +770,7 @@ typedef struct { int target_frame; int len; int *pos; + gboolean found; } FrameDescData; /* @@ -823,7 +831,24 @@ read_enum_value (const char *mem, int type) return 0; } -static gboolean describe_value(MonoType * type, gpointer addr, gboolean expandValueType) +static gboolean +nullable_try_get_value (guint8 *nullable, MonoClass *klass, gpointer* out_value) +{ + mono_class_setup_fields (klass); + g_assert (m_class_is_fields_inited (klass)); + + *out_value = NULL; + MonoClassField *klass_fields = m_class_get_fields (klass); + gpointer addr_for_has_value = mono_vtype_get_field_addr (nullable, &klass_fields[0]); + if (0 == *(guint8*)addr_for_has_value) + return FALSE; + + *out_value = mono_vtype_get_field_addr (nullable, &klass_fields[1]); + return TRUE; +} + +static gboolean +describe_value(MonoType * type, gpointer addr, int gpflags) { ERROR_DECL (error); switch (type->type) { @@ -892,7 +917,45 @@ static gboolean describe_value(MonoType * type, gpointer addr, gboolean expandVa } break; } + + case MONO_TYPE_OBJECT: { + MonoObject *obj = *(MonoObject**)addr; + MonoClass *klass = obj->vtable->klass; + if (!klass) { + // boxed null + mono_wasm_add_obj_var ("object", NULL, 0); + break; + } + + type = m_class_get_byval_arg (klass); + if (type->type == MONO_TYPE_OBJECT) { + mono_wasm_add_obj_var ("object", "object", get_object_id (obj)); + break; + } + + // Boxed valuetype + if (m_class_is_valuetype (klass)) + addr = mono_object_unbox_internal (obj); + + return describe_value (type, addr, gpflags); + } + case MONO_TYPE_GENERICINST: { + MonoClass *klass = mono_class_from_mono_type_internal (type); + if (mono_class_is_nullable (klass)) { + MonoType *targ = type->data.generic_class->context.class_inst->type_argv [0]; + + gpointer nullable_value = NULL; + if (nullable_try_get_value (addr, klass, &nullable_value)) { + return describe_value (targ, nullable_value, gpflags); + } else { + char* class_name = mono_type_full_name (type); + mono_wasm_add_obj_var (class_name, NULL, 0); + g_free (class_name); + break; + } + } + if (mono_type_generic_inst_is_valuetype (type)) goto handle_vtype; /* @@ -902,11 +965,16 @@ static gboolean describe_value(MonoType * type, gpointer addr, gboolean expandVa case MONO_TYPE_SZARRAY: case MONO_TYPE_ARRAY: - case MONO_TYPE_OBJECT: case MONO_TYPE_CLASS: { MonoObject *obj = *(MonoObject**)addr; MonoClass *klass = type->data.klass; + if (m_class_is_valuetype (mono_object_class (obj))) { + addr = mono_object_unbox_internal (obj); + type = m_class_get_byval_arg (mono_object_class (obj)); + goto handle_vtype; + } + char *class_name = mono_type_full_name (type); int obj_id = get_object_id (obj); @@ -923,17 +991,16 @@ static gboolean describe_value(MonoType * type, gpointer addr, gboolean expandVa method = mono_get_delegate_invoke_internal (klass); if (!method) { - DEBUG_PRINTF (2, "Could not get a method for the delegate for %s\n", class_name); - break; - } - - MonoMethod *tm = ((MonoDelegate *)obj)->method; - char *tm_desc = NULL; - if (tm) - tm_desc = mono_method_to_desc_for_js (tm, FALSE); + mono_wasm_add_func_var (class_name, NULL, -1); + } else { + MonoMethod *tm = ((MonoDelegate *)obj)->method; + char *tm_desc = NULL; + if (tm) + tm_desc = mono_method_to_desc_for_js (tm, FALSE); - mono_wasm_add_func_var (class_name, tm_desc, obj_id); - g_free (tm_desc); + mono_wasm_add_func_var (class_name, tm_desc, obj_id); + g_free (tm_desc); + } } else { char *to_string_val = get_to_string_description (class_name, klass, addr); mono_wasm_add_obj_var (class_name, to_string_val, obj_id); @@ -984,7 +1051,7 @@ static gboolean describe_value(MonoType * type, gpointer addr, gboolean expandVa } else { char *to_string_val = get_to_string_description (class_name, klass, addr); - if (expandValueType) { + if (gpflags & GPFLAG_EXPAND_VALUETYPES) { int32_t size = mono_class_value_size (klass, NULL); void *value_buf = g_malloc0 (size); mono_value_copy_internal (value_buf, addr, klass); @@ -996,7 +1063,7 @@ static gboolean describe_value(MonoType * type, gpointer addr, gboolean expandVa g_free (value_buf); // FIXME: isAsyncLocalThis - describe_object_properties_for_klass (addr, klass, FALSE, expandValueType); + describe_object_properties_for_klass (addr, klass, FALSE, gpflags); mono_wasm_add_typed_value ("end_vt", NULL, 0); } else { EM_ASM ({ @@ -1030,7 +1097,7 @@ are_getters_allowed (const char *class_name) return FALSE; } -static void +static gboolean invoke_and_describe_getter_value (MonoObject *obj, MonoProperty *p) { ERROR_DECL (error); @@ -1043,35 +1110,45 @@ invoke_and_describe_getter_value (MonoObject *obj, MonoProperty *p) if (!is_ok (error) && exc == NULL) exc = (MonoObject*) mono_error_convert_to_exception (error); if (exc) - describe_value (mono_get_object_type (), &exc, TRUE); + return describe_value (mono_get_object_type (), &exc, GPFLAG_EXPAND_VALUETYPES); else if (!res || !m_class_is_valuetype (mono_object_class (res))) - describe_value (sig->ret, &res, TRUE); + return describe_value (sig->ret, &res, GPFLAG_EXPAND_VALUETYPES); else - describe_value (sig->ret, mono_object_unbox_internal (res), TRUE); + return describe_value (sig->ret, mono_object_unbox_internal (res), GPFLAG_EXPAND_VALUETYPES); } static void -describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAsyncLocalThis, gboolean expandValueType) +describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAsyncLocalThis, int gpflags) { MonoClassField *f; MonoProperty *p; MonoMethodSignature *sig; - gpointer iter = NULL; gboolean is_valuetype; int pnum; char *klass_name; gboolean auto_invoke_getters; + gboolean is_own; + gboolean only_backing_fields; g_assert (klass); + MonoClass *start_klass = klass; + + only_backing_fields = gpflags & GPFLAG_ACCESSORS_ONLY; is_valuetype = m_class_is_valuetype(klass); + if (is_valuetype) + gpflags |= GPFLAG_EXPAND_VALUETYPES; +handle_parent: + is_own = (start_klass == klass); + klass_name = mono_class_full_name (klass); + gpointer iter = NULL; while (obj && (f = mono_class_get_fields_internal (klass, &iter))) { if (isAsyncLocalThis && f->name[0] == '<' && f->name[1] == '>') { if (g_str_has_suffix (f->name, "__this")) { mono_wasm_add_properties_var ("this", f->offset); gpointer field_value = (guint8*)obj + f->offset; - describe_value (f->type, field_value, is_valuetype | expandValueType); + describe_value (f->type, field_value, gpflags); } continue; @@ -1081,20 +1158,23 @@ describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAs if (mono_field_is_deleted (f)) continue; - mono_wasm_add_properties_var (f->name, f->offset); + if (only_backing_fields && !g_str_has_suffix(f->name, "k__BackingField")) + continue; + + EM_ASM ({ + MONO.mono_wasm_add_properties_var ($0, { field_offset: $1, is_own: $2, attr: $3, owner_class: $4 }); + }, f->name, f->offset, is_own, f->type->attrs, klass_name); gpointer field_addr; if (is_valuetype) field_addr = mono_vtype_get_field_addr (obj, f); else field_addr = (guint8*)obj + f->offset; - - describe_value (f->type, field_addr, is_valuetype | expandValueType); + + describe_value (f->type, field_addr, gpflags); } - klass_name = mono_class_full_name (klass); auto_invoke_getters = are_getters_allowed (klass_name); - iter = NULL; pnum = 0; while ((p = mono_class_get_properties (klass, &iter))) { @@ -1102,8 +1182,15 @@ describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAs if (isAsyncLocalThis && (p->name[0] != '<' || (p->name[0] == '<' && p->name[1] == '>'))) continue; - mono_wasm_add_properties_var (p->name, pnum); sig = mono_method_signature_internal (p->get); + if (sig->param_count != 0) { + // getters with params are not shown + continue; + } + + EM_ASM ({ + MONO.mono_wasm_add_properties_var ($0, { field_offset: $1, is_own: $2, attr: $3, owner_class: $4 }); + }, p->name, pnum, is_own, p->attrs, klass_name); gboolean vt_self_type_getter = is_valuetype && mono_class_from_mono_type_internal (sig->ret) == klass; if (auto_invoke_getters && !vt_self_type_getter) { @@ -1112,8 +1199,7 @@ describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAs // not allowed to call the getter here char *ret_class_name = mono_class_full_name (mono_class_from_mono_type_internal (sig->ret)); - gboolean invokable = sig->param_count == 0; - mono_wasm_add_typed_value ("getter", ret_class_name, invokable); + mono_wasm_add_typed_value ("getter", ret_class_name, -1); g_free (ret_class_name); continue; @@ -1123,6 +1209,14 @@ describe_object_properties_for_klass (void *obj, MonoClass *klass, gboolean isAs } g_free (klass_name); + + // ownProperties + // Note: ownProperties should mean that we return members of the klass itself, + // but we are going to ignore that here, because otherwise vscode/chrome don't + // seem to ask for inherited fields at all. + // if (!is_valuetype && !(gpflags & GPFLAG_OWN_PROPERTIES) && (klass = m_class_get_parent (klass))) + if (!is_valuetype && (klass = m_class_get_parent (klass))) + goto handle_parent; } /* @@ -1148,9 +1242,9 @@ describe_delegate_properties (MonoObject *obj) } static gboolean -describe_object_properties (guint64 objectId, gboolean isAsyncLocalThis, gboolean expandValueType) +describe_object_properties (guint64 objectId, gboolean isAsyncLocalThis, int gpflags) { - DEBUG_PRINTF (2, "describe_object_properties %llu\n", objectId); + DEBUG_PRINTF (2, "describe_object_properties %llu, gpflags: %d\n", objectId, gpflags); MonoObject *obj = get_object_from_id (objectId); if (!obj) @@ -1160,7 +1254,7 @@ describe_object_properties (guint64 objectId, gboolean isAsyncLocalThis, gboolea // delegates get the same id format as regular objects describe_delegate_properties (obj); } else { - describe_object_properties_for_klass (obj, obj->vtable->klass, isAsyncLocalThis, expandValueType); + describe_object_properties_for_klass (obj, obj->vtable->klass, isAsyncLocalThis, gpflags); } return TRUE; @@ -1174,7 +1268,9 @@ invoke_getter (void *obj_or_value, MonoClass *klass, const char *name) return FALSE; } - gpointer iter = NULL; + gpointer iter; +handle_parent: + iter = NULL; MonoProperty *p; while ((p = mono_class_get_properties (klass, &iter))) { //if get doesn't have name means that doesn't have a getter implemented and we don't want to show value, like VS debug @@ -1185,11 +1281,14 @@ invoke_getter (void *obj_or_value, MonoClass *klass, const char *name) return TRUE; } + if ((klass = m_class_get_parent(klass))) + goto handle_parent; + return FALSE; } -static gboolean -describe_array_values (guint64 objectId, int startIdx, int count, gboolean expandValueType) +static gboolean +describe_array_values (guint64 objectId, int startIdx, int count, int gpflags) { if (count == 0) return TRUE; @@ -1229,7 +1328,7 @@ describe_array_values (guint64 objectId, int startIdx, int count, gboolean expan for (int i = startIdx; i < endIdx; i ++) { mono_wasm_add_array_item(i); elem = (gpointer*)((char*)arr->vector + (i * esize)); - describe_value (m_class_get_byval_arg (m_class_get_element_class (klass)), elem, expandValueType); + describe_value (m_class_get_byval_arg (m_class_get_element_class (klass)), elem, gpflags); } return TRUE; } @@ -1237,14 +1336,14 @@ describe_array_values (guint64 objectId, int startIdx, int count, gboolean expan static void describe_async_method_locals (InterpFrame *frame, MonoMethod *method) { - //Async methods are special in the way that local variables can be lifted to generated class fields + //Async methods are special in the way that local variables can be lifted to generated class fields gpointer addr = NULL; if (mono_debug_lookup_method_async_debug_info (method)) { addr = mini_get_interp_callbacks ()->frame_get_this (frame); MonoObject *obj = *(MonoObject**)addr; int objId = get_object_id (obj); mono_wasm_set_is_async_method (objId); - describe_object_properties (objId, TRUE, FALSE); + describe_object_properties (objId, TRUE, GPFLAG_NONE); } } @@ -1264,30 +1363,36 @@ describe_non_async_this (InterpFrame *frame, MonoMethod *method) mono_wasm_add_properties_var ("this", -1); if (m_class_is_valuetype (klass)) { - describe_value (type, obj, TRUE); + describe_value (type, obj, GPFLAG_EXPAND_VALUETYPES); } else { // this is an object, and we can retrieve the valuetypes in it later // through the object id - describe_value (type, addr, FALSE); + describe_value (type, addr, GPFLAG_NONE); } } } static gboolean -describe_variable (InterpFrame *frame, MonoMethod *method, int pos, gboolean expandValueType) +describe_variable (InterpFrame *frame, MonoMethod *method, MonoMethodHeader *header, int pos, int gpflags) { - ERROR_DECL (error); - MonoMethodHeader *header = NULL; - MonoType *type = NULL; gpointer addr = NULL; if (pos < 0) { + MonoMethodSignature *sig = mono_method_signature_internal (method); pos = -pos - 1; - type = mono_method_signature_internal (method)->params [pos]; + + if (pos >= sig->param_count) { + DEBUG_PRINTF(1, "BUG: describe_variable, trying to access param indexed %d, but the method (%s) has only %d params\n", pos, method->name, sig->param_count); + return FALSE; + } + + type = sig->params [pos]; addr = mini_get_interp_callbacks ()->frame_get_arg (frame, pos); } else { - header = mono_method_get_header_checked (method, error); - mono_error_assert_ok (error); /* FIXME report error */ + if (pos >= header->num_locals) { + DEBUG_PRINTF(1, "BUG: describe_variable, trying to access local indexed %d, but the method (%s) has only %d locals\n", pos, method->name, header->num_locals); + return FALSE; + } type = header->locals [pos]; addr = mini_get_interp_callbacks ()->frame_get_local (frame, pos); @@ -1295,41 +1400,45 @@ describe_variable (InterpFrame *frame, MonoMethod *method, int pos, gboolean exp DEBUG_PRINTF (2, "adding val %p type [%p] %s\n", addr, type, mono_type_full_name (type)); - describe_value(type, addr, expandValueType); - if (header) - mono_metadata_free_mh (header); - - return TRUE; + return describe_value(type, addr, gpflags); } static gboolean describe_variables_on_frame (MonoStackFrameInfo *info, MonoContext *ctx, gpointer ud) { + ERROR_DECL (error); FrameDescData *data = (FrameDescData*)ud; + ++data->cur_frame; + //skip wrappers if (info->type != FRAME_TYPE_MANAGED && info->type != FRAME_TYPE_INTERP) { return FALSE; } - if (data->cur_frame < data->target_frame) { - ++data->cur_frame; + if (data->cur_frame != data->target_frame) return FALSE; - } + + data->found = TRUE; InterpFrame *frame = (InterpFrame*)info->interp_frame; g_assert (frame); MonoMethod *method = frame->imethod->method; g_assert (method); + MonoMethodHeader *header = mono_method_get_header_checked (method, error); + mono_error_assert_ok (error); /* FIXME report error */ + for (int i = 0; i < data->len; i++) { - describe_variable (frame, method, data->pos[i], TRUE); + if (!describe_variable (frame, method, header, data->pos[i], GPFLAG_EXPAND_VALUETYPES)) + mono_wasm_add_typed_value("symbol", "", 0); } describe_async_method_locals (frame, method); describe_non_async_this (frame, method); + mono_metadata_free_mh (header); return TRUE; } @@ -1343,39 +1452,42 @@ mono_wasm_get_deref_ptr_value (void *value_addr, MonoClass *klass) } mono_wasm_add_properties_var ("deref", -1); - describe_value (type->data.type, value_addr, TRUE); - return TRUE; + return describe_value (type->data.type, value_addr, GPFLAG_EXPAND_VALUETYPES); } //FIXME this doesn't support getting the return value pseudo-var EMSCRIPTEN_KEEPALIVE gboolean mono_wasm_get_local_vars (int scope, int* pos, int len) { + if (scope < 0) + return FALSE; + FrameDescData data; data.target_frame = scope; - data.cur_frame = 0; + data.cur_frame = -1; data.len = len; data.pos = pos; + data.found = FALSE; mono_walk_stack_with_ctx (describe_variables_on_frame, NULL, MONO_UNWIND_NONE, &data); - return TRUE; + return data.found; } EMSCRIPTEN_KEEPALIVE gboolean -mono_wasm_get_object_properties (int object_id, gboolean expand_value_types) +mono_wasm_get_object_properties (int object_id, int gpflags) { - DEBUG_PRINTF (2, "getting properties of object %d\n", object_id); + DEBUG_PRINTF (2, "getting properties of object %d, gpflags: %d\n", object_id, gpflags); - return describe_object_properties (object_id, FALSE, expand_value_types); + return describe_object_properties (object_id, FALSE, gpflags); } EMSCRIPTEN_KEEPALIVE gboolean -mono_wasm_get_array_values (int object_id, int start_idx, int count, gboolean expand_value_types) +mono_wasm_get_array_values (int object_id, int start_idx, int count, int gpflags) { - DEBUG_PRINTF (2, "getting array values %d, startIdx: %d, count: %d, expandValueType: %d\n", object_id, start_idx, count, expand_value_types); + DEBUG_PRINTF (2, "getting array values %d, startIdx: %d, count: %d, gpflags: 0x%x\n", object_id, start_idx, count, gpflags); - return describe_array_values (object_id, start_idx, count, expand_value_types); + return describe_array_values (object_id, start_idx, count, gpflags); } EMSCRIPTEN_KEEPALIVE gboolean diff --git a/src/mono/netcore/sample/wasm/browser/WasmSample.csproj b/src/mono/netcore/sample/wasm/browser/WasmSample.csproj index e268098b6654d7..304723926fb0b4 100644 --- a/src/mono/netcore/sample/wasm/browser/WasmSample.csproj +++ b/src/mono/netcore/sample/wasm/browser/WasmSample.csproj @@ -36,7 +36,7 @@ new { index = v.Index, name = v.Name }).ToArray(); + return new MonoCommands($"MONO.mono_wasm_eval_member_access({scopeId}, {JsonConvert.SerializeObject(var_ids)}, '', '{expr}')"); + } + public static MonoCommands SetBreakpoint(string assemblyName, uint methodToken, int ilOffset) => new MonoCommands($"MONO.mono_wasm_set_breakpoint (\"{assemblyName}\", {methodToken}, {ilOffset})"); public static MonoCommands RemoveBreakpoint(int breakpointId) => new MonoCommands($"MONO.mono_wasm_remove_breakpoint({breakpointId})"); @@ -285,7 +291,7 @@ internal class ExecutionContext internal DebugStore store; public TaskCompletionSource Source { get; } = new TaskCompletionSource(); - public Dictionary LocalsCache = new Dictionary(); + Dictionary perScopeCaches { get; } = new Dictionary(); public DebugStore Store { @@ -298,11 +304,26 @@ public DebugStore Store } } + public PerScopeCache GetCacheForScope(int scope_id) + { + if (perScopeCaches.TryGetValue(scope_id, out var cache)) + return cache; + + cache = new PerScopeCache(); + perScopeCaches[scope_id] = cache; + return cache; + } + public void ClearState() { CallStack = null; - LocalsCache.Clear(); + perScopeCaches.Clear(); } + } + internal class PerScopeCache + { + public Dictionary Locals { get; } = new Dictionary(); + public Dictionary MemberReferences { get; } = new Dictionary(); } } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs index 9f34078b796dc5..f863a3c8a892fb 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs @@ -6,12 +6,14 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Emit; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Microsoft.WebAssembly.Diagnostics @@ -19,54 +21,30 @@ namespace Microsoft.WebAssembly.Diagnostics internal class EvaluateExpression { - - class FindThisExpression : CSharpSyntaxWalker + class FindVariableNMethodCall : CSharpSyntaxWalker { - public List thisExpressions = new List(); - public SyntaxTree syntaxTree; - public FindThisExpression(SyntaxTree syntax) - { - syntaxTree = syntax; - } + public List identifiers = new List(); + public List methodCall = new List(); + public List memberAccesses = new List(); + public List argValues = new List(); + public override void Visit(SyntaxNode node) { - if (node is ThisExpressionSyntax) + // TODO: PointerMemberAccessExpression + if (node is MemberAccessExpressionSyntax maes + && node.Kind() == SyntaxKind.SimpleMemberAccessExpression + && !(node.Parent is MemberAccessExpressionSyntax)) { - if (node.Parent is MemberAccessExpressionSyntax thisParent && thisParent.Name is IdentifierNameSyntax) - { - IdentifierNameSyntax var = thisParent.Name as IdentifierNameSyntax; - thisExpressions.Add(var.Identifier.Text); - var newRoot = syntaxTree.GetRoot().ReplaceNode(node.Parent, thisParent.Name); - syntaxTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options); - this.Visit(GetExpressionFromSyntaxTree(syntaxTree)); - } + memberAccesses.Add(maes); } - else - base.Visit(node); - } - public async Task CheckIfIsProperty(MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token) - { - foreach (var var in thisExpressions) + if (node is IdentifierNameSyntax identifier + && !(identifier.Parent is MemberAccessExpressionSyntax) + && !identifiers.Any(x => x.Identifier.Text == identifier.Identifier.Text)) { - JToken value = await proxy.TryGetVariableValue(msg_id, scope_id, var, true, token); - if (value == null) - throw new Exception($"The property {var} does not exist in the current context"); + identifiers.Add(identifier); } - } - } - - class FindVariableNMethodCall : CSharpSyntaxWalker - { - public List variables = new List(); - public List thisList = new List(); - public List methodCall = new List(); - public List values = new List(); - public override void Visit(SyntaxNode node) - { - if (node is IdentifierNameSyntax identifier && !variables.Any(x => x.Identifier.Text == identifier.Identifier.Text)) - variables.Add(identifier); if (node is InvocationExpressionSyntax) { methodCall.Add(node as InvocationExpressionSyntax); @@ -76,29 +54,74 @@ public override void Visit(SyntaxNode node) throw new Exception("Assignment is not implemented yet"); base.Visit(node); } - public async Task ReplaceVars(SyntaxTree syntaxTree, MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token) + + public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable ma_values, IEnumerable id_values) { CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot(); - foreach (var var in variables) + var memberAccessToParamName = new Dictionary(); + + // 1. Replace all this.a occurrences with this_a_ABDE + root = root.ReplaceNodes(memberAccesses, (maes, _) => + { + var ma_str = maes.ToString(); + if (!memberAccessToParamName.TryGetValue(ma_str, out var id_name)) + { + // Generate a random suffix + string suffix = Guid.NewGuid().ToString().Substring(0, 5); + string prefix = ma_str.Trim().Replace(".", "_"); + id_name = $"{prefix}_{suffix}"; + + memberAccessToParamName[ma_str] = id_name; + } + + return SyntaxFactory.IdentifierName(id_name); + }); + + var paramsSet = new HashSet(); + + // 2. For every unique member ref, add a corresponding method param + foreach (var (maes, value) in memberAccesses.Zip(ma_values)) { - ClassDeclarationSyntax classDeclaration = root.Members.ElementAt(0) as ClassDeclarationSyntax; - MethodDeclarationSyntax method = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax; + var node_str = maes.ToString(); + if (!memberAccessToParamName.TryGetValue(node_str, out var id_name)) + { + throw new Exception($"BUG: Expected to find an id name for the member access string: {node_str}"); + } - JToken value = await proxy.TryGetVariableValue(msg_id, scope_id, var.Identifier.Text, false, token); + root = UpdateWithNewMethodParam(root, id_name, value); + } - if (value == null) - throw new Exception($"The name {var.Identifier.Text} does not exist in the current context"); + foreach (var (idns, value) in identifiers.Zip(id_values)) + { + root = UpdateWithNewMethodParam(root, idns.Identifier.Text, value); + } - values.Add(ConvertJSToCSharpType(value["value"])); + return syntaxTree.WithRootAndOptions(root, syntaxTree.Options); + + CompilationUnitSyntax UpdateWithNewMethodParam(CompilationUnitSyntax root, string id_name, JObject value) + { + var classDeclaration = root.Members.ElementAt(0) as ClassDeclarationSyntax; + var method = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax; + + if (paramsSet.Contains(id_name)) + { + // repeated member access expression + // eg. this.a + this.a + return root; + } + + argValues.Add(ConvertJSToCSharpType(value)); var updatedMethod = method.AddParameterListParameters( SyntaxFactory.Parameter( - SyntaxFactory.Identifier(var.Identifier.Text)) - .WithType(SyntaxFactory.ParseTypeName(GetTypeFullName(value["value"])))); + SyntaxFactory.Identifier(id_name)) + .WithType(SyntaxFactory.ParseTypeName(GetTypeFullName(value)))); + + paramsSet.Add(id_name); root = root.ReplaceNode(method, updatedMethod); + + return root; } - syntaxTree = syntaxTree.WithRootAndOptions(root, syntaxTree.Options); - return syntaxTree; } private object ConvertJSToCSharpType(JToken variable) @@ -120,7 +143,7 @@ private object ConvertJSToCSharpType(JToken variable) return null; break; } - throw new Exception($"Evaluate of this datatype {type} not implemented yet"); + throw new Exception($"Evaluate of this datatype {type} not implemented yet");//, "Unsupported"); } private string GetTypeFullName(JToken variable) @@ -140,7 +163,7 @@ private string GetTypeFullName(JToken variable) default: return value.GetType().FullName; } - throw new Exception($"Evaluate of this datatype {type} not implemented yet"); + throw new ReturnAsErrorException($"GetTypefullName: Evaluate of this datatype {type} not implemented yet", "Unsupported"); } } @@ -151,36 +174,90 @@ static SyntaxNode GetExpressionFromSyntaxTree(SyntaxTree syntaxTree) MethodDeclarationSyntax methodDeclaration = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax; BlockSyntax blockValue = methodDeclaration.Body; ReturnStatementSyntax returnValue = blockValue.Statements.ElementAt(0) as ReturnStatementSyntax; - InvocationExpressionSyntax expressionInvocation = returnValue.Expression as InvocationExpressionSyntax; - MemberAccessExpressionSyntax expressionMember = expressionInvocation.Expression as MemberAccessExpressionSyntax; - ParenthesizedExpressionSyntax expressionParenthesized = expressionMember.Expression as ParenthesizedExpressionSyntax; - return expressionParenthesized.Expression; + ParenthesizedExpressionSyntax expressionParenthesized = returnValue.Expression as ParenthesizedExpressionSyntax; + + return expressionParenthesized?.Expression; } - internal static async Task CompileAndRunTheExpression(MonoProxy proxy, MessageId msg_id, int scope_id, string expression, CancellationToken token) + private static async Task> ResolveMemberAccessExpressions(IEnumerable member_accesses, + MemberReferenceResolver resolver, CancellationToken token) { - FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall(); - string retString; + var memberAccessValues = new List(); + foreach (var maes in member_accesses) + { + var memberAccessString = maes.ToString(); + var value = await resolver.Resolve(memberAccessString, token); + if (value == null) + throw new ReturnAsErrorException($"Failed to resolve member access for {memberAccessString}", "ReferenceError"); + + memberAccessValues.Add(value); + } + + return memberAccessValues; + } + + private static async Task> ResolveIdentifiers(IEnumerable identifiers, MemberReferenceResolver resolver, CancellationToken token) + { + var values = new List(); + foreach (var var in identifiers) + { + JObject value = await resolver.Resolve(var.Identifier.Text, token); + if (value == null) + throw new ReturnAsErrorException($"The name {var.Identifier.Text} does not exist in the current context", "ReferenceError"); + + values.Add(value); + } + + return values; + } + + internal static async Task CompileAndRunTheExpression(string expression, MemberReferenceResolver resolver, CancellationToken token) + { + expression = expression.Trim(); SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@" - using System; - public class CompileAndRunTheExpression - { - public string Evaluate() - { - return (" + expression + @").ToString(); - } - }"); - - FindThisExpression findThisExpression = new FindThisExpression(syntaxTree); + using System; + public class CompileAndRunTheExpression + { + public static object Evaluate() + { + return (" + expression + @"); + } + }"); + var expressionTree = GetExpressionFromSyntaxTree(syntaxTree); - findThisExpression.Visit(expressionTree); - await findThisExpression.CheckIfIsProperty(proxy, msg_id, scope_id, token); - syntaxTree = findThisExpression.syntaxTree; + if (expressionTree == null) + throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); - expressionTree = GetExpressionFromSyntaxTree(syntaxTree); + FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall(); findVarNMethodCall.Visit(expressionTree); - syntaxTree = await findVarNMethodCall.ReplaceVars(syntaxTree, proxy, msg_id, scope_id, token); + // this fails with `"a)"` + // because the code becomes: return (a)); + // and the returned expression from GetExpressionFromSyntaxTree is `a`! + if (expressionTree.Kind() == SyntaxKind.IdentifierName || expressionTree.Kind() == SyntaxKind.ThisExpression) + { + var var_name = expressionTree.ToString(); + var value = await resolver.Resolve(var_name, token); + if (value == null) + throw new ReturnAsErrorException($"Cannot find member named '{var_name}'.", "ReferenceError"); + + return value; + } + + var memberAccessValues = await ResolveMemberAccessExpressions(findVarNMethodCall.memberAccesses, resolver, token); + + // eg. "this.dateTime", " dateTime.TimeOfDay" + if (expressionTree.Kind() == SyntaxKind.SimpleMemberAccessExpression && findVarNMethodCall.memberAccesses.Count == 1) + { + return memberAccessValues[0]; + } + + var identifierValues = await ResolveIdentifiers(findVarNMethodCall.identifiers, resolver, token); + + syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, memberAccessValues, identifierValues); + expressionTree = GetExpressionFromSyntaxTree(syntaxTree); + if (expressionTree == null) + throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); MetadataReference[] references = new MetadataReference[] { @@ -193,21 +270,83 @@ public string Evaluate() syntaxTrees: new[] { syntaxTree }, references: references, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var semanticModel = compilation.GetSemanticModel(syntaxTree); + var typeInfo = semanticModel.GetTypeInfo(expressionTree); + using (var ms = new MemoryStream()) { EmitResult result = compilation.Emit(ms); + if (!result.Success) + { + var sb = new StringBuilder(); + foreach (var d in result.Diagnostics) + sb.Append(d.ToString()); + + throw new ReturnAsErrorException(sb.ToString(), "CompilationError"); + } + ms.Seek(0, SeekOrigin.Begin); Assembly assembly = Assembly.Load(ms.ToArray()); Type type = assembly.GetType("CompileAndRunTheExpression"); - object obj = Activator.CreateInstance(type); + var ret = type.InvokeMember("Evaluate", - BindingFlags.Default | BindingFlags.InvokeMethod, + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, + null, null, - obj, - findVarNMethodCall.values.ToArray()); - retString = ret.ToString(); + findVarNMethodCall.argValues.ToArray()); + + return JObject.FromObject(ConvertCSharpToJSType(ret, typeInfo.Type)); + } + } + + static readonly HashSet NumericTypes = new HashSet + { + typeof(decimal), typeof(byte), typeof(sbyte), + typeof(short), typeof(ushort), + typeof(int), typeof(uint), + typeof(float), typeof(double) + }; + + static object ConvertCSharpToJSType(object v, ITypeSymbol type) + { + if (v == null) + return new { type = "object", subtype = "null", className = type.ToString() }; + + if (v is string s) + { + return new { type = "string", value = s, description = s }; + } + else if (NumericTypes.Contains(v.GetType())) + { + return new { type = "number", value = v, description = v.ToString() }; } - return retString; + else + { + return new { type = "object", value = v, description = v.ToString(), className = type.ToString() }; + } + } + + } + + class ReturnAsErrorException : Exception + { + public Result Error { get; } + public ReturnAsErrorException(JObject error) + => Error = Result.Err(error); + + public ReturnAsErrorException(string message, string className) + { + Error = Result.Err(JObject.FromObject(new + { + result = new + { + type = "object", + subtype = "error", + description = message, + className + } + })); } } } diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs new file mode 100644 index 00000000000000..145e2237c5e2ab --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.WebAssembly.Diagnostics +{ + internal class MemberReferenceResolver + { + private MessageId messageId; + private int scopeId; + private MonoProxy proxy; + private ExecutionContext ctx; + private PerScopeCache scopeCache; + private VarInfo[] varIds; + private ILogger logger; + private bool locals_fetched = false; + + public MemberReferenceResolver(MonoProxy proxy, ExecutionContext ctx, MessageId msg_id, int scope_id, ILogger logger) + { + messageId = msg_id; + scopeId = scope_id; + this.proxy = proxy; + this.ctx = ctx; + this.logger = logger; + scopeCache = ctx.GetCacheForScope(scope_id); + } + + // Checks Locals, followed by `this` + public async Task Resolve(string var_name, CancellationToken token) + { + if (scopeCache.Locals.Count == 0 && !locals_fetched) + { + var scope_res = await proxy.GetScopeProperties(messageId, scopeId, token); + if (scope_res.IsErr) + throw new Exception($"BUG: Unable to get properties for scope: {scopeId}. {scope_res}"); + locals_fetched = true; + } + + if (scopeCache.Locals.TryGetValue(var_name, out var obj)) + { + return obj["value"]?.Value(); + } + + if (scopeCache.MemberReferences.TryGetValue(var_name, out var ret)) + return ret; + + if (varIds == null) + { + var scope = ctx.CallStack.FirstOrDefault(s => s.Id == scopeId); + varIds = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset); + } + + var res = await proxy.SendMonoCommand(messageId, MonoCommands.EvaluateMemberAccess(scopeId, var_name, varIds), token); + if (res.IsOk) + { + ret = res.Value?["result"]?["value"]?["value"]?.Value(); + scopeCache.MemberReferences[var_name] = ret; + } + else + { + logger.LogDebug(res.Error.ToString()); + } + + return ret; + } + + } +} diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs index 4166c7ef236655..a0cc9dc8926c46 100644 --- a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -170,7 +170,6 @@ protected override async Task AcceptCommand(MessageId id, string method, J case "Debugger.enable": { - System.Console.WriteLine("recebi o Debugger.enable"); var resp = await SendCommand(id, method, args, token); context.DebuggerId = resp.Value["debuggerId"]?.ToString(); @@ -426,7 +425,9 @@ protected override async Task AcceptCommand(MessageId id, string method, J async Task RuntimeGetProperties(MessageId id, DotnetObjectId objectId, JToken args, CancellationToken token) { if (objectId.Scheme == "scope") + { return await GetScopeProperties(id, int.Parse(objectId.Value), token); + } var res = await SendMonoCommand(id, MonoCommands.GetDetails(objectId, args), token); if (res.IsErr) @@ -456,7 +457,6 @@ async Task RuntimeGetProperties(MessageId id, DotnetObjectId objectId, J return res; } - //static int frame_id=0; async Task OnPause(SessionId sessionId, JObject args, CancellationToken token) { //FIXME we should send release objects every now and then? Or intercept those we inject and deal in the runtime @@ -517,12 +517,11 @@ async Task OnPause(SessionId sessionId, JObject args, CancellationToken to } var frames = new List(); - int frame_id = 0; var the_mono_frames = res.Value?["result"]?["value"]?["frames"]?.Values(); foreach (var mono_frame in the_mono_frames) { - ++frame_id; + int frame_id = mono_frame["frame_id"].Value(); var il_pos = mono_frame["il_pos"].Value(); var method_token = mono_frame["method_token"].Value(); var assembly_name = mono_frame["assembly_name"].Value(); @@ -559,12 +558,12 @@ async Task OnPause(SessionId sessionId, JObject args, CancellationToken to Log("info", $"frame il offset: {il_pos} method token: {method_token} assembly name: {assembly_name}"); Log("info", $"\tmethod {method_name} location: {location}"); - frames.Add(new Frame(method, location, frame_id - 1)); + frames.Add(new Frame(method, location, frame_id)); callFrames.Add(new { functionName = method_name, - callFrameId = $"dotnet:scope:{frame_id - 1}", + callFrameId = $"dotnet:scope:{frame_id}", functionLocation = method.StartLocation.AsLocation(), location = location.AsLocation(), @@ -581,7 +580,7 @@ async Task OnPause(SessionId sessionId, JObject args, CancellationToken to @type = "object", className = "Object", description = "Object", - objectId = $"dotnet:scope:{frame_id-1}", + objectId = $"dotnet:scope:{frame_id}", }, name = method_name, startLocation = method.StartLocation.AsLocation(), @@ -673,64 +672,6 @@ async Task Step(MessageId msg_id, StepKind kind, CancellationToken token) return true; } - internal bool TryFindVariableValueInCache(ExecutionContext ctx, string expression, bool only_search_on_this, out JToken obj) - { - if (ctx.LocalsCache.TryGetValue(expression, out obj)) - { - if (only_search_on_this && obj["fromThis"] == null) - return false; - return true; - } - return false; - } - - internal async Task TryGetVariableValue(MessageId msg_id, int scope_id, string expression, bool only_search_on_this, CancellationToken token) - { - JToken thisValue = null; - var context = GetContext(msg_id); - if (context.CallStack == null) - return null; - - if (TryFindVariableValueInCache(context, expression, only_search_on_this, out JToken obj)) - return obj; - - var scope = context.CallStack.FirstOrDefault(s => s.Id == scope_id); - var live_vars = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset); - //get_this - var res = await SendMonoCommand(msg_id, MonoCommands.GetScopeVariables(scope.Id, live_vars), token); - - var scope_values = res.Value?["result"]?["value"]?.Values()?.ToArray(); - thisValue = scope_values?.FirstOrDefault(v => v["name"]?.Value() == "this"); - - if (!only_search_on_this) - { - if (thisValue != null && expression == "this") - return thisValue; - - var value = scope_values.SingleOrDefault(sv => sv["name"]?.Value() == expression); - if (value != null) - return value; - } - - //search in scope - if (thisValue != null) - { - if (!DotnetObjectId.TryParse(thisValue["value"]["objectId"], out var objectId)) - return null; - - res = await SendMonoCommand(msg_id, MonoCommands.GetDetails(objectId), token); - scope_values = res.Value?["result"]?["value"]?.Values().ToArray(); - var foundValue = scope_values.FirstOrDefault(v => v["name"].Value() == expression); - if (foundValue != null) - { - foundValue["fromThis"] = true; - context.LocalsCache[foundValue["name"].Value()] = foundValue; - return foundValue; - } - } - return null; - } - async Task OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string expression, CancellationToken token) { try @@ -739,35 +680,40 @@ async Task OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string ex if (context.CallStack == null) return false; - var varValue = await TryGetVariableValue(msg_id, scope_id, expression, false, token); + var resolver = new MemberReferenceResolver(this, context, msg_id, scope_id, logger); - if (varValue != null) + JObject retValue = await resolver.Resolve(expression, token); + if (retValue == null) + { + retValue = await EvaluateExpression.CompileAndRunTheExpression(expression, resolver, token); + } + + if (retValue != null) { SendResponse(msg_id, Result.OkFromObject(new { - result = varValue["value"] + result = retValue }), token); - return true; } - - string retValue = await EvaluateExpression.CompileAndRunTheExpression(this, msg_id, scope_id, expression, token); - SendResponse(msg_id, Result.OkFromObject(new + else { - result = new - { - value = retValue - } - }), token); - return true; + SendResponse(msg_id, Result.Err($"Unable to evaluate '{expression}'"), token); + } + } + catch (ReturnAsErrorException ree) + { + SendResponse(msg_id, ree.Error, token); } catch (Exception e) { - logger.LogDebug(e, $"Error in EvaluateOnCallFrame for expression '{expression}."); + logger.LogDebug($"Error in EvaluateOnCallFrame for expression '{expression}' with '{e}."); + SendResponse(msg_id, Result.Exception(e), token); } - return false; + + return true; } - async Task GetScopeProperties(MessageId msg_id, int scope_id, CancellationToken token) + internal async Task GetScopeProperties(MessageId msg_id, int scope_id, CancellationToken token) { try { @@ -788,8 +734,11 @@ async Task GetScopeProperties(MessageId msg_id, int scope_id, Cancellati if (values == null || values.Length == 0) return Result.OkFromObject(new { result = Array.Empty() }); + var frameCache = ctx.GetCacheForScope(scope_id); foreach (var value in values) - ctx.LocalsCache[value["name"]?.Value()] = value; + { + frameCache.Locals[value["name"]?.Value()] = value; + } return Result.OkFromObject(new { result = values }); } @@ -902,7 +851,7 @@ async Task RemoveBreakpoint(MessageId msg_id, JObject args, CancellationToken to bp.State = BreakpointState.Disabled; } } - breakpointRequest.Locations.Clear(); + context.BreakpointRequests.Remove(bpid); } async Task SetBreakpoint(SessionId sessionId, DebugStore store, BreakpointRequest req, bool sendResolvedEvent, CancellationToken token) diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs index d6169102bb12b8..09094cea2212ff 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs @@ -272,7 +272,7 @@ public async Task RunOnVTArray(bool roundtrip) => await RunCallFunctionOn( var act_i_props = await GetProperties(act_i["value"]["objectId"]?.Value()); await CheckProps(act_i_props, new { - dt = TValueType("System.DateTime", new DateTime(2020 + (i * 2), 1, 2, 3, 4, 5).ToString()), + dt = TDateTime(new DateTime(2020 + (i * 2), 1, 2, 3, 4, 5)), gs = TValueType("Math.GenericStruct") }, "obj_own ss_arr[{i}]"); @@ -320,12 +320,10 @@ public async Task RunOnCFOValueTypeResult(bool roundtrip) => await RunCallFuncti var dt = new DateTime(2020, 1, 2, 3, 4, 5); await CheckProps(obj_own_val, new { - dt = TValueType("System.DateTime", dt.ToString()), + dt = TDateTime(dt), gs = TValueType("Math.GenericStruct") }, $"obj_own-props"); - await CheckDateTime(obj_own_val, "dt", dt); - var gs_props = await GetObjectOnLocals(obj_own_val, "gs"); await CheckProps(gs_props, new { @@ -649,16 +647,9 @@ public async Task PropertyGettersTest(string eval_fn, string method_name, int li // Auto properties show w/o getters, because they have // a backing field - DTAutoProperty = TValueType("System.DateTime", dt.ToString()) + DTAutoProperty = TDateTime(dt) }, local_name); - // Automatic properties don't have invokable getters, because we can get their - // value from the backing field directly - { - var dt_auto_props = await GetObjectOnLocals(obj_props, "DTAutoProperty"); - await CheckDateTime(obj_props, "DTAutoProperty", dt); - } - // Invoke getters, and check values dt = new DateTime(3, 4, 5, 6, 7, 8); @@ -669,8 +660,7 @@ public async Task PropertyGettersTest(string eval_fn, string method_name, int li await CheckValue(res.Value["result"], JObject.FromObject(new { type = "string", value = $"String property, V: 0xDEADBEEF" }), $"{local_name}.String"); res = await InvokeGetter(obj, get_args_fn(new[] { "DT" }), cfo_fn); - await CheckValue(res.Value["result"], TValueType("System.DateTime", dt.ToString()), $"{local_name}.DT"); - await CheckDateTimeValue(res.Value["result"], dt); + await CheckValue(res.Value["result"], TDateTime(dt), $"{local_name}.DT"); // Check arrays through getters @@ -696,8 +686,8 @@ public async Task PropertyGettersTest(string eval_fn, string method_name, int li var arr_elems = await GetProperties(res.Value["result"]?["objectId"]?.Value()); var exp_elems = new[] { - TValueType("System.DateTime", dt0.ToString()), - TValueType("System.DateTime", dt1.ToString()), + TDateTime(dt0), + TDateTime(dt1) }; await CheckProps(arr_elems, exp_elems, $"{local_name}.DTArray"); @@ -707,6 +697,38 @@ public async Task PropertyGettersTest(string eval_fn, string method_name, int li } }); + [Fact] + public async Task InvokeInheritedAndPrivateGetters() => await CheckInspectLocalsAtBreakpointSite( + $"DebuggerTests.GetPropertiesTests.DerivedClass", "InstanceMethod", 1, "InstanceMethod", + $"window.setTimeout(function() {{ invoke_static_method_async ('[debugger-test] DebuggerTests.GetPropertiesTests.DerivedClass:run'); }})", + wait_for_event_fn: async (pause_location) => + { + var frame_id = pause_location["callFrames"][0]["callFrameId"].Value(); + var frame_locals = await GetProperties(frame_id); + var this_obj = GetAndAssertObjectWithName(frame_locals, "this"); + + var args = new[] + { + // private + ("_DTProp", TDateTime(new DateTime(2200, 5, 6, 7, 8, 9))), + + // overridden + ("DateTimeForOverride", TDateTime(new DateTime(2190, 9, 7, 5, 3, 2))), + ("FirstName", TString("DerivedClass#FirstName")), + ("StringPropertyForOverrideWithAutoProperty", TString("DerivedClass#StringPropertyForOverrideWithAutoProperty")), + + // inherited + ("_base_dateTime", TDateTime(new DateTime(2134, 5, 7, 1, 9, 2))) + }; + + foreach (var (name, expected) in args) + { + var res = await InvokeGetter(this_obj, name); + await CheckValue(res.Value["result"], expected, name); + } + }); + + [Theory] [InlineData("invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 38, 12, true)] [InlineData("invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 38, 12, false)] diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs index dad28dff6f3d15..2218b9a7945114 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Microsoft.WebAssembly.Diagnostics; @@ -10,205 +12,494 @@ namespace DebuggerTests { - + // TODO: static async, static method args public class EvaluateOnCallFrameTests : DebuggerTestBase { + public static IEnumerable InstanceMethodsTestData(string type_name) + { + yield return new object[] { type_name, "InstanceMethod", "InstanceMethod", false }; + yield return new object[] { type_name, "GenericInstanceMethod", "GenericInstanceMethod", false }; + yield return new object[] { type_name, "InstanceMethodAsync", "MoveNext", true }; + yield return new object[] { type_name, "GenericInstanceMethodAsync", "MoveNext", true }; - [Fact] - public async Task EvaluateThisProperties() => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16, - "run", - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + // TODO: { "DebuggerTests.EvaluateTestsGeneric`1", "Instance", 9, "EvaluateTestsGenericStructInstanceMethod", prefix } + } + + public static IEnumerable InstanceMethodForTypeMembersTestData(string type_name) + { + foreach (var data in InstanceMethodsTestData(type_name)) + { + yield return new object[] { "", 0 }.Concat(data).ToArray(); + yield return new object[] { "this.", 0 }.Concat(data).ToArray(); + yield return new object[] { "NewInstance.", 3 }.Concat(data).ToArray(); + yield return new object[] { "this.NewInstance.", 3 }.Concat(data).ToArray(); + } + } + + [Theory] + [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")] + [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")] + public async Task EvaluateTypeInstanceMembers(string prefix, int bias, string type, string method, string bp_function_name, bool is_async) + => await CheckInspectLocalsAtBreakpointSite( + type, method, /*line_offset*/1, bp_function_name, + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] {type}:run');}}, 1);", wait_for_event_fn: async (pause_location) => { - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); - var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "a"); - CheckContentValue(evaluate, "1"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "b"); - CheckContentValue(evaluate, "2"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "c"); - CheckContentValue(evaluate, "3"); - - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "dt"); - await CheckDateTimeValue(evaluate, new DateTime(2000, 5, 4, 3, 2, 1)); + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + var dateTime = new DateTime(2010, 9, 8, 7, 6, 5 + bias); + var DTProp = dateTime.AddMinutes(10); + + foreach (var pad in new[] { String.Empty, " " }) + { + var padded_prefix = pad + prefix; + await EvaluateOnCallFrameAndCheck(id, + ($"{padded_prefix}a", TNumber(4)), + + // fields + ($"{padded_prefix}dateTime.TimeOfDay", TValueType("System.TimeSpan", dateTime.TimeOfDay.ToString())), + ($"{padded_prefix}dateTime", TDateTime(dateTime)), + ($"{padded_prefix}dateTime.TimeOfDay.Minutes", TNumber(dateTime.TimeOfDay.Minutes)), + + // properties + ($"{padded_prefix}DTProp.TimeOfDay.Minutes", TNumber(DTProp.TimeOfDay.Minutes)), + ($"{padded_prefix}DTProp", TDateTime(DTProp)), + ($"{padded_prefix}DTProp.TimeOfDay", TValueType("System.TimeSpan", DTProp.TimeOfDay.ToString())), + + ($"{padded_prefix}IntProp", TNumber(9)), + ($"{padded_prefix}NullIfAIsNotZero", TObject("DebuggerTests.EvaluateTestsClassWithProperties", is_null: true)) + ); + } }); [Theory] - [InlineData(63, 12, "EvaluateTestsStructInstanceMethod")] - [InlineData(79, 12, "GenericInstanceMethodOnStruct")] - [InlineData(102, 12, "EvaluateTestsGenericStructInstanceMethod")] - public async Task EvaluateThisPropertiesOnStruct(int line, int col, string method_name) => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-evaluate-test.cs", line, col, - method_name, - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + [MemberData(nameof(InstanceMethodsTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")] + [MemberData(nameof(InstanceMethodsTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")] + public async Task EvaluateInstanceMethodArguments(string type, string method, string bp_function_name, bool is_async) + => await CheckInspectLocalsAtBreakpointSite( + type, method, /*line_offset*/1, bp_function_name, + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] {type}:run');}}, 1);", wait_for_event_fn: async (pause_location) => { - var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "a"); - CheckContentValue(evaluate, "1"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "b"); - CheckContentValue(evaluate, "2"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "c"); - CheckContentValue(evaluate, "3"); - - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "dateTime"); - await CheckDateTimeValue(evaluate, new DateTime(2020, 1, 2, 3, 4, 5)); + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + var DTProp = new DateTime(2010, 9, 8, 7, 6, 5).AddMinutes(10); + await EvaluateOnCallFrameAndCheck(id, + ("g", TNumber(400)), + ("h", TNumber(123)), + ("valString", TString("just a test")), + ("me", TObject(type)), + + // property on method arg + ("me.DTProp", TDateTime(DTProp)), + ("me.DTProp.TimeOfDay.Minutes", TNumber(DTProp.TimeOfDay.Minutes)), + ("me.DTProp.Second + (me.IntProp - 5)", TNumber(DTProp.Second + 4))); + }); + + [Theory] + [MemberData(nameof(InstanceMethodsTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")] + [MemberData(nameof(InstanceMethodsTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")] + public async Task EvaluateMethodLocals(string type, string method, string bp_function_name, bool is_async) + => await CheckInspectLocalsAtBreakpointSite( + type, method, /*line_offset*/5, bp_function_name, + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] {type}:run');}}, 1);", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + var dt = new DateTime(2025, 3, 5, 7, 9, 11); + await EvaluateOnCallFrameAndCheck(id, + (" d ", TNumber(401)), + ("d", TNumber(401)), + (" d", TNumber(401)), + ("e", TNumber(402)), + ("f", TNumber(403)), + + // property on a local + ("local_dt", TDateTime(dt)), + (" local_dt", TDateTime(dt)), + ("local_dt.Date", TDateTime(dt.Date)), + (" local_dt.Date", TDateTime(dt.Date))); }); [Fact] - public async Task EvaluateParameters() => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16, - "run", - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + public async Task EvaluateStaticLocalsWithDeepMemberAccess() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateTestsClass", "EvaluateLocals", 9, "EvaluateLocals", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); - var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "g"); - CheckContentValue(evaluate, "100"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "h"); - CheckContentValue(evaluate, "200"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "valString"); - CheckContentValue(evaluate, "test"); + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + var dt = new DateTime(2020, 1, 2, 3, 4, 5); + await EvaluateOnCallFrameAndCheck(id, + ("f_s.c", TNumber(4)), + ("f_s", TValueType("DebuggerTests.EvaluateTestsStructWithProperties")), + + ("f_s.dateTime", TDateTime(dt)), + ("f_s.dateTime.Date", TDateTime(dt.Date))); }); [Fact] - public async Task EvaluateLocals() => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16, - "run", - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + public async Task EvaluateLocalsAsync() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.Point", "AsyncInstanceMethod", 1, "MoveNext", + "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod', true); })", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + // sc_arg + { + var (sc_arg, _) = await EvaluateOnCallFrame(id, "sc_arg"); + await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), nameof(sc_arg)); + + // Check that we did get the correct object + var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value()); + await CheckProps(sc_arg_props, new + { + X = TNumber(10), + Y = TNumber(45), + Id = TString("sc#Id"), + Color = TEnum("DebuggerTests.RGB", "Blue"), + PointWithCustomGetter = TGetter("PointWithCustomGetter") + }, "sc_arg_props#1"); + + await EvaluateOnCallFrameAndCheck(id, + ("(sc_arg.PointWithCustomGetter.X)", TNumber(100)), + ("sc_arg.Id + \"_foo\"", TString($"sc#Id_foo")), + ("sc_arg.Id + (sc_arg.X==10 ? \"_is_ten\" : \"_not_ten\")", TString($"sc#Id_is_ten"))); + } + + // local_gs + { + var (local_gs, _) = await EvaluateOnCallFrame(id, "local_gs"); + await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct"), nameof(local_gs)); + + (local_gs, _) = await EvaluateOnCallFrame(id, " local_gs"); + await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct"), nameof(local_gs)); + + var local_gs_props = await GetProperties(local_gs["objectId"]?.Value()); + await CheckProps(local_gs_props, new + { + Id = TObject("string", is_null: true), + Color = TEnum("DebuggerTests.RGB", "Red"), + Value = TNumber(0) + }, "local_gs_props#1"); + await EvaluateOnCallFrameAndCheck(id, ("(local_gs.Id)", TString(null))); + } + }); + + [Theory] + [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsStructWithProperties")] + [MemberData(nameof(InstanceMethodForTypeMembersTestData), parameters: "DebuggerTests.EvaluateTestsClassWithProperties")] + public async Task EvaluateExpressionsWithDeepMemberAccesses(string prefix, int bias, string type, string method, string bp_function_name, bool _) + => await CheckInspectLocalsAtBreakpointSite( + type, method, /*line_offset*/4, bp_function_name, + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] {type}:run');}}, 1);", wait_for_event_fn: async (pause_location) => { - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); - var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "d"); - CheckContentValue(evaluate, "101"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "e"); - CheckContentValue(evaluate, "102"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "f"); - CheckContentValue(evaluate, "103"); - - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "local_dt"); - await CheckDateTimeValue(evaluate, new DateTime(2010, 9, 8, 7, 6, 5)); + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + var dateTime = new DateTime(2010, 9, 8, 7, 6, 5 + bias); + var DTProp = dateTime.AddMinutes(10); + + await EvaluateOnCallFrameAndCheck(id, + ($"{prefix}a + 5", TNumber(9)), + ($"10 + {prefix}IntProp", TNumber(19)), + ($" {prefix}IntProp + {prefix}DTProp.Second", TNumber(9 + DTProp.Second)), + ($" {prefix}IntProp + ({prefix}DTProp.Second+{prefix}dateTime.Year)", TNumber(9 + DTProp.Second + dateTime.Year)), + ($" {prefix}DTProp.Second > 0 ? \"_foo_\": \"_zero_\"", TString("_foo_")), + + // local_dt is not live yet + ($"local_dt.Date.Year * 10", TNumber(10))); + }); + + [Theory] + [InlineData("")] + [InlineData("this.")] + public async Task InheritedAndPrivateMembersInAClass(string prefix) + => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.GetPropertiesTests.DerivedClass", "InstanceMethod", 1, "InstanceMethod", + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] DebuggerTests.GetPropertiesTests.DerivedClass:run');}}, 1);", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + foreach(var pad in new [] { String.Empty, " "}) + { + var padded_prefix = pad + prefix; + await EvaluateOnCallFrameAndCheck(id, + // overridden + ($"{padded_prefix}FirstName + \"_foo\"", TString("DerivedClass#FirstName_foo")), + ($"{padded_prefix}DateTimeForOverride.Date.Year", TNumber(2190)), + ($"{padded_prefix}DateTimeForOverride.Date.Year - 10", TNumber(2180)), + ($"\"foo_\" + {padded_prefix}StringPropertyForOverrideWithAutoProperty", TString("foo_DerivedClass#StringPropertyForOverrideWithAutoProperty")), + + // private + ($"{padded_prefix}_stringField + \"_foo\"", TString("DerivedClass#_stringField_foo")), + ($"{padded_prefix}_stringField", TString("DerivedClass#_stringField")), + ($"{padded_prefix}_dateTime.Second + 4", TNumber(7)), + ($"{padded_prefix}_DTProp.Second + 4", TNumber(13)), + + // inherited public + ($"\"foo_\" + {padded_prefix}Base_AutoStringProperty", TString("foo_base#Base_AutoStringProperty")), + // inherited private + ($"{padded_prefix}_base_dateTime.Date.Year - 10", TNumber(2124)) + ); + } }); [Fact] - public async Task EvaluateLocalsAsync() + public async Task EvaluateSimpleExpressions() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateTestsClass/TestEvaluate", "run", 9, "run", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + await EvaluateOnCallFrameAndCheck(id, + // "((this))", TObject("foo")); //FIXME: + // "((dt))", TObject("foo")); //FIXME: + + ("this", TObject("DebuggerTests.EvaluateTestsClass.TestEvaluate")), + (" this", TObject("DebuggerTests.EvaluateTestsClass.TestEvaluate")), + + ("5", TNumber(5)), + (" 5", TNumber(5)), + ("d + e", TNumber(203)), + ("e + 10", TNumber(112)), + + // repeated expressions + ("this.a + this.a", TNumber(2)), + ("a + \"_\" + a", TString("9000_9000")), + ("a+(a )", TString("90009000")), + + // possible duplicate arg name + ("this.a + this_a", TNumber(46)), + + ("this.a + this.b", TNumber(3)), + ("\"test\" + \"test\"", TString("testtest")), + ("5 + 5", TNumber(10))); + }); + + public static TheoryData ShadowMethodArgsTestData => new TheoryData { - var bp_loc = "dotnet://debugger-test.dll/debugger-array-test.cs"; - int line = 249; - int col = 12; - var function_name = "MoveNext"; - await CheckInspectLocalsAtBreakpointSite( - bp_loc, line, col, - function_name, - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod', true); })", - wait_for_event_fn: async (pause_location) => + { "DebuggerTests.EvaluateTestsClassWithProperties", "EvaluateShadow", "EvaluateShadow" }, + { "DebuggerTests.EvaluateTestsClassWithProperties", "EvaluateShadowAsync", "MoveNext" }, + { "DebuggerTests.EvaluateTestsStructWithProperties", "EvaluateShadow", "EvaluateShadow" }, + { "DebuggerTests.EvaluateTestsStructWithProperties", "EvaluateShadowAsync", "MoveNext" }, + }; + + [Theory] + [MemberData(nameof(ShadowMethodArgsTestData))] + public async Task LocalsAndArgsShadowingThisMembers(string type_name, string method, string bp_function_name) => await CheckInspectLocalsAtBreakpointSite( + type_name, method, 2, bp_function_name, + "window.setTimeout(function() { invoke_static_method ('[debugger-test] " + type_name + ":run'); })", + wait_for_event_fn: async (pause_location) => + { + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + await EvaluateOnCallFrameAndCheck(id, + ("a", TString("hello")), + ("this.a", TNumber(4))); + + await CheckExpressions("this.", new DateTime(2010, 9, 8, 7, 6, 5 + 0)); + await CheckExpressions(String.Empty, new DateTime(2020, 3, 4, 5, 6, 7)); + + async Task CheckExpressions(string prefix, DateTime dateTime) { - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + await EvaluateOnCallFrameAndCheck(id, + (prefix + "dateTime", TDateTime(dateTime)), + (prefix + "dateTime.TimeOfDay.Minutes", TNumber(dateTime.TimeOfDay.Minutes)), + (prefix + "dateTime.TimeOfDay", TValueType("System.TimeSpan", dateTime.TimeOfDay.ToString()))); + } + }); - // sc_arg - { - var sc_arg = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "sc_arg"); - await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), "sc_arg#1"); - - var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value()); - await CheckProps(sc_arg_props, new - { - X = TNumber(10), - Y = TNumber(45), - Id = TString("sc#Id"), - Color = TEnum("DebuggerTests.RGB", "Blue"), - PointWithCustomGetter = TGetter("PointWithCustomGetter") - }, "sc_arg_props#1"); - } - - // local_gs - { - var local_gs = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "local_gs"); - await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct"), "local_gs#1"); - - var local_gs_props = await GetProperties(local_gs["objectId"]?.Value()); - await CheckProps(local_gs_props, new - { - Id = TObject("string", is_null: true), - Color = TEnum("DebuggerTests.RGB", "Red"), - Value = TNumber(0) - }, "local_gs_props#1"); - } - - // step, check local_gs - pause_location = await StepAndCheck(StepKind.Over, bp_loc, line + 1, col, function_name); - { - var local_gs = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "local_gs"); - await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct"), "local_gs#2"); - - var local_gs_props = await GetProperties(local_gs["objectId"]?.Value()); - await CheckProps(local_gs_props, new - { - Id = TString("local_gs#Id"), - Color = TEnum("DebuggerTests.RGB", "Green"), - Value = TNumber(4) - }, "local_gs_props#2"); - } - - // step check sc_arg.Id - pause_location = await StepAndCheck(StepKind.Over, bp_loc, line + 2, col, function_name); - { - var sc_arg = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "sc_arg"); - await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), "sc_arg#2"); - - var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value()); - await CheckProps(sc_arg_props, new - { - X = TNumber(10), - Y = TNumber(45), - Id = TString("sc_arg#Id"), // <------- This changed - Color = TEnum("DebuggerTests.RGB", "Blue"), - PointWithCustomGetter = TGetter("PointWithCustomGetter") - }, "sc_arg_props#2"); - } - }); + [Theory] + [InlineData("DebuggerTests.EvaluateTestsStructWithProperties", true)] + [InlineData("DebuggerTests.EvaluateTestsClassWithProperties", false)] + public async Task EvaluateOnPreviousFrames(string type_name, bool is_valuetype) => await CheckInspectLocalsAtBreakpointSite( + type_name, "EvaluateShadow", 1, "EvaluateShadow", + $"window.setTimeout(function() {{ invoke_static_method ('[debugger-test] {type_name}:run'); }})", + wait_for_event_fn: async (pause_location) => + { + var dt_local = new DateTime(2020, 3, 4, 5, 6, 7); + var dt_this = new DateTime(2010, 9, 8, 7, 6, 5); + + // At EvaluateShadow + { + var id0 = pause_location["callFrames"][0]["callFrameId"].Value(); + await EvaluateOnCallFrameAndCheck(id0, + ("dateTime", TDateTime(dt_local)), + ("this.dateTime", TDateTime(dt_this)) + ); + + await EvaluateOnCallFrameFail(id0, ("obj.IntProp", "ReferenceError")); + } + + { + var id1 = pause_location["callFrames"][1]["callFrameId"].Value(); + await EvaluateOnCallFrameFail(id1, + ("dateTime", "ReferenceError"), + ("this.dateTime", "ReferenceError")); + + // obj available only on the -1 frame + await EvaluateOnCallFrameAndCheck(id1, ("obj.IntProp", TNumber(7))); + } + + await SetBreakpointInMethod("debugger-test.dll", type_name, "SomeMethod", 1); + pause_location = await SendCommandAndCheck(null, "Debugger.resume", null, 0, 0, "SomeMethod"); + + // At SomeMethod + + // TODO: change types also.. so, that `this` is different! + + // Check frame0 + { + var id0 = pause_location["callFrames"][0]["callFrameId"].Value(); + + // 'me' and 'dateTime' are reversed in this method + await EvaluateOnCallFrameAndCheck(id0, + ("dateTime", is_valuetype ? TValueType(type_name) : TObject(type_name)), + ("this.dateTime", TDateTime(dt_this)), + ("me", TDateTime(dt_local)), + + // local variable shadows field, but isn't "live" yet + ("DTProp", TString(null)), + + // access field via `this.` + ("this.DTProp", TDateTime(dt_this.AddMinutes(10)))); + + await EvaluateOnCallFrameFail(id0, ("obj", "ReferenceError")); + } + + // check frame1 + { + var id1 = pause_location["callFrames"][1]["callFrameId"].Value(); + + await EvaluateOnCallFrameAndCheck(id1, + // 'me' and 'dateTime' are reversed in this method + ("dateTime", TDateTime(dt_local)), + ("this.dateTime", TDateTime(dt_this)), + ("me", is_valuetype ? TValueType(type_name) : TObject(type_name)), + + // not shadowed here + ("DTProp", TDateTime(dt_this.AddMinutes(10))), + + // access field via `this.` + ("this.DTProp", TDateTime(dt_this.AddMinutes(10)))); + + await EvaluateOnCallFrameFail(id1, ("obj", "ReferenceError")); + } + + // check frame2 + { + var id2 = pause_location["callFrames"][2]["callFrameId"].Value(); + + // Only obj should be available + await EvaluateOnCallFrameFail(id2, + ("dateTime", "ReferenceError"), + ("this.dateTime", "ReferenceError"), + ("me", "ReferenceError")); + + await EvaluateOnCallFrameAndCheck(id2, ("obj", is_valuetype ? TValueType(type_name) : TObject(type_name))); + } + }); + + [Fact] + public async Task JSEvaluate() + { + var insp = new Inspector(); + //Collect events + var scripts = SubscribeToScripts(insp); + + var bp_loc = "/other.js"; + var line = 76; + var col = 1; + + await Ready(); + await insp.Ready(async (cli, token) => + { + ctx = new DebugTestContext(cli, insp, token, scripts); + await SetBreakpoint(bp_loc, line, col); + + var eval_expr = "window.setTimeout(function() { eval_call_on_frame_test (); }, 1)"; + var result = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), ctx.token); + var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE); + + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + await EvaluateOnCallFrameFail(id, + ("me.foo", null), + ("obj.foo.bar", null)); + + await EvaluateOnCallFrame(id, "obj.foo", expect_ok: true); + }); } [Fact] - public async Task EvaluateExpressions() => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16, - "run", - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + public async Task NegativeTestsInInstanceMethod() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateTestsClass/TestEvaluate", "run", 9, "run", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); - var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "d + e"); - CheckContentValue(evaluate, "203"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "e + 10"); - CheckContentValue(evaluate, "112"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "a + a"); - CheckContentValue(evaluate, "2"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "this.a + this.b"); - CheckContentValue(evaluate, "3"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "\"test\" + \"test\""); - CheckContentValue(evaluate, "testtest"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "5 + 5"); - CheckContentValue(evaluate, "10"); + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + // Use '.' on a primitive member + await EvaluateOnCallFrameFail(id, + //BUG: TODO: + //("a)", "CompilationError"), + + ("this.a.", "ReferenceError"), + ("a.", "ReferenceError"), + + ("this..a", "CompilationError"), + (".a.", "ReferenceError"), + + ("me.foo", "ReferenceError"), + + ("this.a + non_existant", "ReferenceError"), + + ("this.NullIfAIsNotZero.foo", "ReferenceError"), + ("NullIfAIsNotZero.foo", "ReferenceError")); }); [Fact] - public async Task EvaluateThisExpressions() => await CheckInspectLocalsAtBreakpointSite( - "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16, - "run", - "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", + public async Task NegativeTestsInStaticMethod() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.EvaluateTestsClass", "EvaluateLocals", 9, "EvaluateLocals", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })", wait_for_event_fn: async (pause_location) => { - var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); - var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "this.a"); - CheckContentValue(evaluate, "1"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "this.b"); - CheckContentValue(evaluate, "2"); - evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "this.c"); - CheckContentValue(evaluate, "3"); - - // FIXME: not supported yet - // evaluate = await EvaluateOnCallFrame (pause_location ["callFrames"][0] ["callFrameId"].Value (), "this.dt"); - // await CheckDateTimeValue (evaluate, new DateTime (2000, 5, 4, 3, 2, 1)); + var id = pause_location["callFrames"][0]["callFrameId"].Value(); + + await EvaluateOnCallFrameFail(id, + ("me.foo", "ReferenceError"), + ("this", "ReferenceError"), + ("this.NullIfAIsNotZero.foo", "ReferenceError")); }); + + async Task EvaluateOnCallFrameAndCheck(string call_frame_id, params (string expression, JObject expected)[] args) + { + foreach (var arg in args) + { + var (eval_val, _) = await EvaluateOnCallFrame(call_frame_id, arg.expression); + try + { + await CheckValue(eval_val, arg.expected, arg.expression); + } + catch + { + Console.WriteLine($"CheckValue failed for {arg.expression}. Expected: {arg.expected}, vs {eval_val}"); + throw; + } + } + } + + async Task EvaluateOnCallFrameFail(string call_frame_id, params (string expression, string class_name)[] args) + { + foreach (var arg in args) + { + var (_, res) = await EvaluateOnCallFrame(call_frame_id, arg.expression, expect_ok: false); + if (arg.class_name != null) + AssertEqual(arg.class_name, res.Error["result"]?["className"]?.Value(), $"Error className did not match for expression '{arg.expression}'"); + } + } } } diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs index 415956a6c2e3ba..6f339c28ea90fd 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/ExceptionTests.cs @@ -19,8 +19,6 @@ public async Task ExceptionTestAll() var insp = new Inspector(); //Collect events var scripts = SubscribeToScripts(insp); - int line = 15; - int col = 20; string entry_method_name = "[debugger-test] DebuggerTests.ExceptionTestsClass:TestExceptions"; await Ready(); diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/GetPropertiesTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/GetPropertiesTests.cs new file mode 100644 index 00000000000000..2f35e8cfdc0e09 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/GetPropertiesTests.cs @@ -0,0 +1,388 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.WebAssembly.Diagnostics; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace DebuggerTests +{ + public class GetPropertiesTests : DebuggerTestBase + { + public static TheoryData, bool> ClassGetPropertiesTestData(bool is_async) + { + var data = new TheoryData, bool>(); + + var type_name = "DerivedClass"; + var all_props = new Dictionary() + { + {"_stringField", (TString("DerivedClass#_stringField"), true)}, + {"_dateTime", (TDateTime(new DateTime(2020, 7, 6, 5, 4, 3)), true)}, + {"_DTProp", (TGetter("_DTProp"), true)}, + + // own public + {"a", (TNumber(4), true)}, + {"DateTime", (TGetter("DateTime"), true)}, + {"AutoStringProperty", (TString("DerivedClass#AutoStringProperty"), true)}, + {"FirstName", (TGetter("FirstName"), true)}, + {"DateTimeForOverride", (TGetter("DateTimeForOverride"), true)}, + + {"StringPropertyForOverrideWithAutoProperty", (TString("DerivedClass#StringPropertyForOverrideWithAutoProperty"), true)}, + {"Base_AutoStringPropertyForOverrideWithField", (TString("DerivedClass#Base_AutoStringPropertyForOverrideWithField"), true)}, + {"Base_GetterForOverrideWithField", (TString("DerivedClass#Base_GetterForOverrideWithField"), true)}, + {"BaseBase_MemberForOverride", (TString("DerivedClass#BaseBase_MemberForOverride"), true)}, + + // indexers don't show up in getprops + // {"Item", (TSymbol("int { get; }"), true)}, + + // inherited private + {"_base_name", (TString("private_name"), false)}, + {"_base_dateTime", (TGetter("_base_dateTime"), false)}, + + // inherited public + {"Base_AutoStringProperty", (TString("base#Base_AutoStringProperty"), false)}, + {"base_num", (TNumber(5), false)}, + {"LastName", (TGetter("LastName"), false)} + }; + + // default, all properties + // n, n + data.Add(type_name, null, null, all_props.Keys.ToArray(), all_props, is_async); + // f, f + data.Add(type_name, false, false, all_props.Keys.ToArray(), all_props, is_async); + // f, n + data.Add(type_name, false, null, all_props.Keys.ToArray(), all_props, is_async); + // n, f + data.Add(type_name, null, false, all_props.Keys.ToArray(), all_props, is_async); + + // all own + // t, f + // t, n + foreach (bool? accessors in new bool?[] { false, null }) + { + // Breaking from JS behavior, we return *all* members irrespective of `ownMembers` + data.Add(type_name, true, accessors, all_props.Keys.ToArray(), all_props, is_async); + // data.Add(type_name, true, accessors, new[] + // { + // "_stringField", + // "_dateTime", + // "_DTProp", + // "a", + // "DateTime", + // "AutoStringProperty", + // "FirstName", + // "DateTimeForOverride", + // "StringPropertyForOverrideWithAutoProperty" + // }, all_props, is_async); + } + + var all_accessors = new[] + { + "_DTProp", + "DateTime", + "_base_dateTime", + "FirstName", + "LastName", + "DateTimeForOverride" + }; + + var only_own_accessors = new[] + { + "_DTProp", + "DateTime", + "FirstName", + "DateTimeForOverride" + }; + + // all own, only accessors + // t, t + + // Breaking from JS behavior, we return *all* members irrespective of `ownMembers` + // data.Add(type_name, true, true, only_own_accessors, all_props, is_async); + data.Add(type_name, true, true, all_accessors, all_props, is_async); + + // all accessors + // f, t + // n, t + foreach (bool? own in new bool?[] { false, null }) + { + data.Add(type_name, own, true, all_accessors, all_props, is_async); + } + + return data; + } + + public static TheoryData, bool> StructGetPropertiesTestData(bool is_async) + { + var data = new TheoryData, bool>(); + + var type_name = "CloneableStruct"; + var all_props = new Dictionary() + { + {"_stringField", (TString("CloneableStruct#_stringField"), true)}, + {"_dateTime", (TDateTime(new DateTime(2020, 7, 6, 5, 4, 3 + 3)), true)}, + {"_DTProp", (TGetter("_DTProp"), true)}, + + // own public + {"a", (TNumber(4), true)}, + {"DateTime", (TGetter("DateTime"), true)}, + {"AutoStringProperty", (TString("CloneableStruct#AutoStringProperty"), true)}, + {"FirstName", (TGetter("FirstName"), true)}, + {"LastName", (TGetter("LastName"), true)}, + + // indexers don't show up in getprops + // {"Item", (TSymbol("int { get; }"), true)} + }; + + // default, all properties + data.Add(type_name, null, null, all_props.Keys.ToArray(), all_props, is_async); + data.Add(type_name, false, false, all_props.Keys.ToArray(), all_props, is_async); + + // all own + data.Add(type_name, true, false, all_props.Keys.ToArray(), all_props, is_async); + + var all_accessor_names = new[] + { + "_DTProp", + "DateTime", + "FirstName", + "LastName" + }; + + // all own, only accessors + data.Add(type_name, true, true, all_accessor_names, all_props, is_async); + // all accessors + data.Add(type_name, false, true, all_accessor_names, all_props, is_async); + + return data; + } + + [Theory] + [MemberData(nameof(ClassGetPropertiesTestData), parameters: true)] + [MemberData(nameof(ClassGetPropertiesTestData), parameters: false)] + [MemberData(nameof(StructGetPropertiesTestData), parameters: true)] + [MemberData(nameof(StructGetPropertiesTestData), parameters: false)] + public async Task InspectTypeInheritedMembers(string type_name, bool? own_properties, bool? accessors_only, string[] expected_names, Dictionary all_props, bool is_async) => await CheckInspectLocalsAtBreakpointSite( + $"DebuggerTests.GetPropertiesTests.{type_name}", + $"InstanceMethod{(is_async ? "Async" : "")}", 1, (is_async ? "MoveNext" : "InstanceMethod"), + $"window.setTimeout(function() {{ invoke_static_method_async ('[debugger-test] DebuggerTests.GetPropertiesTests.{type_name}:run'); }})", + wait_for_event_fn: async (pause_location) => + { + var frame_id = pause_location["callFrames"][0]["callFrameId"].Value(); + var frame_locals = await GetProperties(frame_id); + var this_obj = GetAndAssertObjectWithName(frame_locals, "this"); + var this_props = await GetProperties(this_obj["value"]?["objectId"]?.Value(), own_properties: own_properties, accessors_only: accessors_only); + + await CheckExpectedProperties(expected_names, name => GetAndAssertObjectWithName(this_props, name), all_props); + + // indexer properties shouldn't show up here + var item = this_props.FirstOrDefault(jt => jt["name"]?.Value() == "Item"); + Assert.Null(item); + + // Useful for debugging: AssertHasOnlyExpectedProperties(expected_names, this_props.Values()); + AssertEqual(expected_names.Length, this_props.Count(), $"expected number of properties"); + }); + + public static IEnumerable MembersForLocalNestedStructData(bool is_async) + => StructGetPropertiesTestData(false).Select (datum => datum [1..]); + + [Theory] + [MemberData(nameof(MembersForLocalNestedStructData), parameters: false)] + [MemberData(nameof(MembersForLocalNestedStructData), parameters: true)] + public async Task MembersForLocalNestedStruct(bool? own_properties, bool? accessors_only, string[] expected_names, Dictionary all_props, bool is_async) => await CheckInspectLocalsAtBreakpointSite( + $"DebuggerTests.GetPropertiesTests.NestedStruct", + is_async ? $"TestNestedStructStaticAsync" : "TestNestedStructStatic", + 2, + is_async ? "MoveNext" : $"TestNestedStructStatic", + $"window.setTimeout(function() {{ invoke_static_method_async ('[debugger-test] DebuggerTests.GetPropertiesTests.NestedStruct:run'); }})", + wait_for_event_fn: async (pause_location) => + { + var ns_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ns"); + + var cs_obj = GetAndAssertObjectWithName(ns_props, "cloneableStruct"); + var cs_props = await GetProperties(cs_obj["value"]?["objectId"]?.Value(), own_properties: own_properties, accessors_only: accessors_only); + + await CheckExpectedProperties(expected_names, name => GetAndAssertObjectWithName(cs_props, name), all_props); + AssertHasOnlyExpectedProperties(expected_names, cs_props.Values()); + + // indexer properties shouldn't show up here + var item = cs_props.FirstOrDefault(jt => jt["name"]?.Value() == "Item"); + Assert.Null(item); + + // Useful for debugging: AssertHasOnlyExpectedProperties(expected_names, this_props.Values()); + AssertEqual(expected_names.Length, cs_props.Count(), $"expected number of properties"); + }); + + public static TheoryData JSGetPropertiesTestData(bool test_js)=> new TheoryData + { + // default, no args set + { + test_js, + null, null, new[] + { + "owner_name", + "owner_last_name", + "kind", + "make", + "available" + } + }, + + // all props + { + test_js, + false, false, new[] + { + "owner_name", + "owner_last_name", + "kind", + "make", + "available" + } + }, + + // all own + { + test_js, + true, false, new[] + { + "owner_name", + "owner_last_name" + } + }, + + // all own accessors + { + test_js, + true, true, new[] + { + "owner_last_name" + } + }, + + // all accessors + { + test_js, + false, true, new[] + { + "available", + "owner_last_name" + } + } + }; + + [Theory] + [MemberData(nameof(JSGetPropertiesTestData), parameters: true)] + // Note: Disabled because we don't match JS's behavior here! + // We return inherited members too for `ownProperties:true` + // [MemberData(nameof(JSGetPropertiesTestData), parameters: false)] + public async Task GetPropertiesTestJSAndManaged(bool test_js, bool? own_properties, bool? accessors_only, string[] expected_names) + { + var insp = new Inspector(); + //Collect events + var scripts = SubscribeToScripts(insp); + + + await Ready(); + await insp.Ready(async (cli, token) => + { + ctx = new DebugTestContext(cli, insp, token, scripts); + string eval_expr; + if (test_js) + { + await SetBreakpoint("/other.js", 93, 1); + eval_expr = "window.setTimeout(function() { get_properties_test (); }, 1)"; + } + else + { + await SetBreakpointInMethod("debugger-test.dll", "DebuggerTests.GetPropertiesTests.DerivedClassForJSTest", "run", 2); + eval_expr = "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.GetPropertiesTests.DerivedClassForJSTest:run'); }, 1)"; + } + + var result = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), ctx.token); + var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE); + + var id = pause_location["callFrames"][0]["scopeChain"][0]["object"]["objectId"].Value(); + + var frame_locals = await GetProperties(id); + var obj = GetAndAssertObjectWithName(frame_locals, "obj"); + var obj_props = await GetProperties(obj["value"]?["objectId"]?.Value(), + own_properties: own_properties, accessors_only: accessors_only); + + IEnumerable filtered_props; + if (test_js) + { + filtered_props = obj_props.Children().Where(jt => jt["enumerable"]?.Value() == true); + } + else + { + // we don't set `enumerable` right now + filtered_props = obj_props.Children().Where(jt=> true); + } + + var expected_props = new Dictionary () + { + // own + {"owner_name", (TString("foo"), true)}, + {"owner_last_name", (TGetter("owner_last_name"), true)}, + + // inherited + {"kind", (TString("car"), false)}, + {"make", (TString("mini"), false)}, + {"available", (TGetter("available"), false)}, + }; + + await CheckExpectedProperties( + expected_names, + name => filtered_props.Where(jt => jt["name"]?.Value () == name).SingleOrDefault(), + expected_props); + + AssertEqual(expected_names.Length, filtered_props.Count(), $"expected number of properties"); + }); + } + + private async Task CheckExpectedProperties(string[] expected_names, Func get_actual_prop, Dictionary all_props) + { + foreach (var exp_name in expected_names) + { + if (!all_props.TryGetValue(exp_name, out var expected)) + { + Assert.True(false, $"Test Bug: Could not find property named {exp_name}"); + } + var (exp_prop, is_own) = expected; + var actual_prop = get_actual_prop(exp_name); + + AssertEqual(is_own, actual_prop["isOwn"]?.Value () == true, $"{exp_name}#isOwn"); + + if (exp_prop["__custom_type"]?.Value() == "getter") + { + // HACK! CheckValue normally expects to get a value:{} + // from `{name: "..", value: {}, ..} + // but for getters we actually have: `{name: "..", get: {..} }` + // and no `value` + await CheckValue(actual_prop, exp_prop, exp_name); + } + else + { + await CheckValue(actual_prop["value"], exp_prop, exp_name); + } + } + } + + private static void AssertHasOnlyExpectedProperties (string[] expected_names, IEnumerable actual) + { + var exp = new HashSet(expected_names); + + foreach (var obj in actual) + { + if (!exp.Contains(obj["name"]?.Value ())) + Console.WriteLine ($"Unexpected: {obj}"); + } + } + + } +} diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/MonoJsTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/MonoJsTests.cs new file mode 100644 index 00000000000000..ca530287ccacf6 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/MonoJsTests.cs @@ -0,0 +1,142 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace DebuggerTests +{ + public class MonoJsTests : DebuggerTestBase + { + [Fact] + public async Task FixupNameValueObjectsWithMissingParts() + { + var insp = new Inspector(); + var scripts = SubscribeToScripts(insp); + + await Ready(); + await insp.Ready(async (cli, token) => + { + ctx = new DebugTestContext(cli, insp, token, scripts); + + var bp1_res = await SetBreakpointInMethod("debugger-test.dll", "Math", "IntAdd", 3); + + var names = new JObject[] + { + JObject.FromObject(new { name = "Abc" }), + JObject.FromObject(new { name = "Def" }), + JObject.FromObject(new { name = "Xyz" }) + }; + + var values = new JObject[] + { + JObject.FromObject(new { value = TObject("testclass") }), + JObject.FromObject(new { value = TString("test string") }), + }; + + var getters = new JObject[] + { + GetterRes("xyz"), + GetterRes("unattached") + }; + + var list = new[] { names[0], names[1], values[0], names[2], getters[0], getters[1] }; + var res = await ctx.cli.SendCommand($"Runtime.evaluate", JObject.FromObject(new { expression = $"MONO._fixup_name_value_objects({JsonConvert.SerializeObject(list)})", returnByValue = true }), ctx.token); + Assert.True(res.IsOk); + + await CheckProps(res.Value["result"]["value"], new + { + Abc = TSymbol(""), + Def = TObject("testclass"), + Xyz = TGetter("xyz") + }, "#1", num_fields: 4); + + JObject.DeepEquals(getters[1], res.Value["result"]["value"].Values().ToArray()[3]); + + JObject GetterRes(string name) => JObject.FromObject(new + { + get = new + { + className = "Function", + description = $"get {name} () {{}}", + type = "function" + } + }); + }); + } + + [Fact] + public async Task GetParamsAndLocalsWithInvalidIndices() + { + var insp = new Inspector(); + var scripts = SubscribeToScripts(insp); + + await Ready(); + await insp.Ready(async (cli, token) => + { + ctx = new DebugTestContext(cli, insp, token, scripts); + + var bp1_res = await SetBreakpointInMethod("debugger-test.dll", "Math", "IntAdd", 3); + var pause_location = await EvaluateAndCheck( + "window.setTimeout(function() { invoke_static_method('[debugger-test] Math:IntAdd', 1, 2); })", + null, -1, -1, "IntAdd"); + + var scope_id = pause_location["callFrames"][0]["callFrameId"].Value(); + var scope = int.Parse(scope_id.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries)[2]); + + var var_ids = new[] + { + new { index = 0, name = "one" }, + new { index = -12, name = "bad0" }, + new { index = 1231, name = "bad1" } + }; + + var expression = $"MONO.mono_wasm_get_variables({scope}, {JsonConvert.SerializeObject(var_ids)})"; + + var res = await ctx.cli.SendCommand($"Runtime.evaluate", JObject.FromObject(new { expression, returnByValue = true }), ctx.token); + Assert.True(res.IsOk); + + await CheckProps(res.Value["result"]?["value"], new + { + one = TNumber(3), + bad0 = TSymbol(""), + bad1 = TSymbol("") + }, "results"); + }); + } + + [Fact] + public async Task InvalidScopeId() + { + var insp = new Inspector(); + var scripts = SubscribeToScripts(insp); + + await Ready(); + await insp.Ready(async (cli, token) => + { + ctx = new DebugTestContext(cli, insp, token, scripts); + + var bp1_res = await SetBreakpointInMethod("debugger-test.dll", "Math", "IntAdd", 3); + await EvaluateAndCheck( + "window.setTimeout(function() { invoke_static_method('[debugger-test] Math:IntAdd', 1, 2); })", + null, -1, -1, "IntAdd"); + + var var_ids = new[] + { + new { index = 0, name = "one" }, + }; + + var scope_id = "-12"; + var expression = $"MONO.mono_wasm_get_variables({scope_id}, {JsonConvert.SerializeObject(var_ids)})"; + var res = await ctx.cli.SendCommand($"Runtime.evaluate", JObject.FromObject(new { expression, returnByValue = true }), ctx.token); + Assert.False(res.IsOk); + + scope_id = "30000"; + expression = $"MONO.mono_wasm_get_variables({scope_id}, {JsonConvert.SerializeObject(var_ids)})"; + res = await ctx.cli.SendCommand($"Runtime.evaluate", JObject.FromObject(new { expression, returnByValue = true }), ctx.token); + Assert.False(res.IsOk); + }); + } + } +} diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs index d18f054d9d7875..ee703b29648bb1 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs @@ -18,8 +18,8 @@ public class PointerTests : DebuggerTestBase new TheoryData { { $"invoke_static_method ('[debugger-test] DebuggerTests.PointerTests:LocalPointers');", "DebuggerTests.PointerTests", "LocalPointers", 32, "LocalPointers", false }, { $"invoke_static_method ('[debugger-test] DebuggerTests.PointerTests:LocalPointers');", "DebuggerTests.PointerTests", "LocalPointers", 32, "LocalPointers", true }, - { $"invoke_static_method_async ('[debugger-test] DebuggerTests.PointerTests:LocalPointersAsync');", "DebuggerTests.PointerTests", "LocalPointersAsync", 32, "MoveNext", false }, - { $"invoke_static_method_async ('[debugger-test] DebuggerTests.PointerTests:LocalPointersAsync');", "DebuggerTests.PointerTests", "LocalPointersAsync", 32, "MoveNext", true } + { $"invoke_static_method_async ('[debugger-test] DebuggerTests.PointerTests:LocalPointersAsync');", "DebuggerTests.PointerTests", "LocalPointersAsync", 32, "LocalPointersAsync", false }, + { $"invoke_static_method_async ('[debugger-test] DebuggerTests.PointerTests:LocalPointersAsync');", "DebuggerTests.PointerTests", "LocalPointersAsync", 32, "LocalPointersAsync", true } }; [Theory] @@ -155,7 +155,7 @@ public async Task InspectLocalPointersToValueTypes(string eval_fn, string type, var dt = new DateTime(5, 6, 7, 8, 9, 10); await CheckProps(locals, new { - dt = TValueType("System.DateTime", dt.ToString()), + dt = TDateTime(dt), dtp = TPointer("System.DateTime*"), dtp_null = TPointer("System.DateTime*", is_null: true), @@ -163,8 +163,6 @@ public async Task InspectLocalPointersToValueTypes(string eval_fn, string type, gsp_null = TPointer("DebuggerTests.GenericStructWithUnmanagedT*") }, "locals", num_fields: 26); - await CheckDateTime(locals, "dt", dt); - // *dtp var props = await GetObjectOnLocals(locals, "dtp"); await CheckDateTime(props, "*dtp", dt); @@ -178,7 +176,7 @@ public async Task InspectLocalPointersToValueTypes(string eval_fn, string type, var gsp_deref_props = await GetObjectOnLocals(gsp_props, "*gsp"); await CheckProps(gsp_deref_props, new { - Value = TValueType("System.DateTime", gs_dt.ToString()), + Value = TDateTime(gs_dt), IntField = TNumber(4), DTPP = TPointer("System.DateTime**") }, "locals#gsp#deref"); @@ -201,7 +199,7 @@ public async Task InspectLocalPointersToValueTypes(string eval_fn, string type, var gsp_deref_props = await GetObjectOnLocals(gsp_w_n_props, "*gsp_null"); await CheckProps(gsp_deref_props, new { - Value = TValueType("System.DateTime", gs_dt.ToString()), + Value = TDateTime(gs_dt), IntField = TNumber(4), DTPP = TPointer("System.DateTime**") }, "locals#gsp#deref"); @@ -282,7 +280,7 @@ public async Task InspectLocalPointersToGenericValueTypeArrays(string eval_fn, s var gsp_deref_props = await GetObjectOnLocals(actual_elems[1], "*[1]"); await CheckProps(gsp_deref_props, new { - Value = TValueType("System.DateTime", gs_dt.ToString()), + Value = TDateTime(gs_dt), IntField = TNumber(4), DTPP = TPointer("System.DateTime**") }, "locals#gsp#deref"); @@ -300,7 +298,7 @@ public async Task InspectLocalPointersToGenericValueTypeArrays(string eval_fn, s var gsp_deref_props = await GetObjectOnLocals(actual_elems[2], "*[2]"); await CheckProps(gsp_deref_props, new { - Value = TValueType("System.DateTime", gs_dt.ToString()), + Value = TDateTime(gs_dt), IntField = TNumber(4), DTPP = TPointer("System.DateTime**") }, "locals#gsp#deref"); @@ -494,8 +492,8 @@ public async Task InspectValueTypePointersAsMethodArgs(string eval_fn, string ty { var actual_elems = await CheckArrayElements(dtpa_elems, new[] { - TValueType("System.DateTime", dt.ToString()), - null + TDateTime(dt), + null }); await CheckDateTime(actual_elems[0], "*[0]", dt); diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs b/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs index d7df33589913d7..c4ec6daae3e582 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs @@ -14,6 +14,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; +using Xunit.Sdk; namespace DebuggerTests { @@ -221,7 +222,6 @@ await EvaluateAndCheck( var top_frame = pause_location["callFrames"][0]; var scope = top_frame["scopeChain"][0]; - Assert.Equal("dotnet:scope:0", scope["object"]["objectId"]); if (wait_for_event_fn != null) await wait_for_event_fn(pause_location); else @@ -270,7 +270,6 @@ await insp.Ready(async (cli, token) => var top_frame = pause_location["callFrames"][0]; var scope = top_frame["scopeChain"][0]; - Assert.Equal("dotnet:scope:0", scope["object"]["objectId"]); if (wait_for_event_fn != null) await wait_for_event_fn(pause_location); @@ -338,25 +337,31 @@ internal async Task CheckPointerValue(JToken locals, string name, JToken return l; } - internal async Task CheckDateTime(JToken locals, string name, DateTime expected) + internal async Task CheckDateTime(JToken value, DateTime expected, string label = "") { - var obj = GetAndAssertObjectWithName(locals, name); - await CheckDateTimeValue(obj["value"], expected); + await CheckValue(value, TValueType("System.DateTime", expected.ToString()), label); + await CheckDateTimeValue(value, expected, label); } - internal async Task CheckDateTimeValue(JToken value, DateTime expected) + internal async Task CheckDateTime(JToken locals, string name, DateTime expected, string label="") { - await CheckDateTimeMembers(value, expected); + var obj = GetAndAssertObjectWithName(locals, name, label); + await CheckDateTimeValue(obj["value"], expected, label); + } + + internal async Task CheckDateTimeValue(JToken value, DateTime expected, string label="") + { + await CheckDateTimeMembers(value, expected, label); var res = await InvokeGetter(JObject.FromObject(new { value = value }), "Date"); - await CheckDateTimeMembers(res.Value["result"], expected.Date); + await CheckDateTimeMembers(res.Value["result"], expected.Date, label); // FIXME: check some float properties too - async Task CheckDateTimeMembers(JToken v, DateTime exp_dt) + async Task CheckDateTimeMembers(JToken v, DateTime exp_dt, string label="") { - AssertEqual("System.DateTime", v["className"]?.Value(), "className"); - AssertEqual(exp_dt.ToString(), v["description"]?.Value(), "description"); + AssertEqual("System.DateTime", v["className"]?.Value(), $"{label}#className"); + AssertEqual(exp_dt.ToString(), v["description"]?.Value(), $"{label}#description"); var members = await GetProperties(v["objectId"]?.Value()); @@ -402,11 +407,11 @@ internal void CheckArray(JToken locals, string name, string class_name, int leng GetAndAssertObjectWithName(locals, name)["value"], TArray(class_name, length), name).Wait(); - internal JToken GetAndAssertObjectWithName(JToken obj, string name) + internal JToken GetAndAssertObjectWithName(JToken obj, string name, string label="") { var l = obj.FirstOrDefault(jt => jt["name"]?.Value() == name); if (l == null) - Assert.True(false, $"Could not find variable '{name}'"); + Assert.True(false, $"[{label}] Could not find variable '{name}'"); return l; } @@ -606,7 +611,7 @@ internal async Task CheckCustomType(JToken actual_val, JToken exp_val, string la { // For getter, `actual_val` is not `.value`, instead it's the container object // which has a `.get` instead of a `.value` - var get = actual_val["get"]; + var get = actual_val?["get"]; Assert.True(get != null, $"[{label}] No `get` found. {(actual_val != null ? "Make sure to pass the container object for testing getters, and not the ['value']" : String.Empty)}"); AssertEqual("Function", get["className"]?.Value(), $"{label}-className"); @@ -616,6 +621,13 @@ internal async Task CheckCustomType(JToken actual_val, JToken exp_val, string la break; } + case "datetime": + { + var dateTime = DateTime.FromBinary(exp_val["binary"].Value()); + await CheckDateTime(actual_val, dateTime, label); + break; + } + case "ignore_me": // nothing to check ;) break; @@ -705,30 +717,34 @@ internal async Task CheckValue(JToken actual_val, JToken exp_val, string label) return; } - foreach (var jp in exp_val.Values()) + try { - if (jp.Value.Type == JTokenType.Object) + foreach (var jp in exp_val.Values()) { - var new_val = await GetProperties(actual_val["objectId"].Value()); - await CheckProps(new_val, jp.Value, $"{label}-{actual_val["objectId"]?.Value()}"); - - continue; - } + if (jp.Value.Type == JTokenType.Object) + { + var new_val = await GetProperties(actual_val["objectId"].Value()); + await CheckProps(new_val, jp.Value, $"{label}-{actual_val["objectId"]?.Value()}"); - var exp_val_str = jp.Value.Value(); - bool null_or_empty_exp_val = String.IsNullOrEmpty(exp_val_str); + continue; + } - var actual_field_val = actual_val.Values().FirstOrDefault(a_jp => a_jp.Name == jp.Name); - var actual_field_val_str = actual_field_val?.Value?.Value(); - if (null_or_empty_exp_val && String.IsNullOrEmpty(actual_field_val_str)) - continue; + var exp_val_str = jp.Value.Value(); + bool null_or_empty_exp_val = String.IsNullOrEmpty(exp_val_str); - Assert.True(actual_field_val != null, $"[{label}] Could not find value field named {jp.Name}"); + var actual_field_val = actual_val?.Values()?.FirstOrDefault(a_jp => a_jp.Name == jp.Name); + var actual_field_val_str = actual_field_val?.Value?.Value(); + if (null_or_empty_exp_val && String.IsNullOrEmpty(actual_field_val_str)) + continue; - Assert.True(exp_val_str == actual_field_val_str, - $"[{label}] Value for json property named {jp.Name} didn't match.\n" + - $"Expected: {jp.Value.Value()}\n" + - $"Actual: {actual_field_val.Value.Value()}"); + Assert.True(actual_field_val != null, $"[{label}] Could not find value field named {jp.Name}"); + AssertEqual(exp_val_str, actual_field_val_str, $"[{label}] Value for json property named {jp.Name} didn't match."); + } + } + catch + { + Console.WriteLine ($"Expected: {exp_val}. Actual: {actual_val}"); + throw; } } @@ -774,7 +790,7 @@ internal async Task GetObjectOnLocals(JToken locals, string name) } /* @fn_args is for use with `Runtime.callFunctionOn` only */ - internal async Task GetProperties(string id, JToken fn_args = null, bool expect_ok = true) + internal async Task GetProperties(string id, JToken fn_args = null, bool? own_properties = null, bool? accessors_only = null, bool expect_ok = true) { if (ctx.UseCallFunctionOnBeforeGetProperties && !id.StartsWith("dotnet:scope:")) { @@ -798,6 +814,14 @@ internal async Task GetProperties(string id, JToken fn_args = null, bool { objectId = id }); + if (own_properties.HasValue) + { + get_prop_req["ownProperties"] = own_properties.Value; + } + if (accessors_only.HasValue) + { + get_prop_req["accessorPropertiesOnly"] = accessors_only.Value; + } var frame_props = await ctx.cli.SendCommand("Runtime.getProperties", get_prop_req, ctx.token); AssertEqual(expect_ok, frame_props.IsOk, $"Runtime.getProperties returned {frame_props.IsOk} instead of {expect_ok}, for {get_prop_req}, with Result: {frame_props}"); @@ -822,7 +846,7 @@ internal async Task GetProperties(string id, JToken fn_args = null, bool return locals; } - internal async Task EvaluateOnCallFrame(string id, string expression) + internal async Task<(JToken, Result)> EvaluateOnCallFrame(string id, string expression, bool expect_ok = true) { var evaluate_req = JObject.FromObject(new { @@ -830,12 +854,25 @@ internal async Task EvaluateOnCallFrame(string id, string expression) expression = expression }); - var frame_evaluate = await ctx.cli.SendCommand("Debugger.evaluateOnCallFrame", evaluate_req, ctx.token); - if (!frame_evaluate.IsOk) - Assert.True(false, $"Debugger.evaluateOnCallFrame failed for {evaluate_req.ToString()}, with Result: {frame_evaluate}"); + var res = await ctx.cli.SendCommand("Debugger.evaluateOnCallFrame", evaluate_req, ctx.token); + AssertEqual(expect_ok, res.IsOk, $"Debugger.evaluateOnCallFrame ('{expression}', scope: {id}) returned {res.IsOk} instead of {expect_ok}, with Result: {res}"); + if (res.IsOk) + return (res.Value["result"], res); - var evaluate_result = frame_evaluate.Value["result"]; - return evaluate_result; + return (null, res); + } + + internal async Task RemoveBreakpoint(string id, bool expect_ok = true) + { + var remove_bp = JObject.FromObject(new + { + breakpointId = id + }); + + var res = await ctx.cli.SendCommand("Debugger.removeBreakpoint", remove_bp, ctx.token); + Assert.True(expect_ok ? res.IsOk : res.IsErr); + + return res; } internal async Task SetBreakpoint(string url_key, int line, int column, bool expect_ok = true, bool use_regex = false) @@ -880,10 +917,15 @@ internal async Task SetBreakpointInMethod(string assembly, string type, return res; } - internal void AssertEqual(object expected, object actual, string label) => Assert.True(expected?.Equals(actual), - $"[{label}]\n" + - $"Expected: {expected?.ToString()}\n" + - $"Actual: {actual?.ToString()}\n"); + internal void AssertEqual(object expected, object actual, string label) + { + if (expected?.Equals(actual) == true) + return; + + throw new AssertActualExpectedException( + expected, actual, + $"[{label}]\n"); + } internal void AssertStartsWith(string expected, string actual, string label) => Assert.True(actual?.StartsWith(expected), $"[{label}] Does not start with the expected string\nExpected: {expected}\nActual: {actual}"); @@ -907,7 +949,7 @@ internal void AssertEqual(object expected, object actual, string label) => Asser //FIXME: um maybe we don't need to convert jobject right here! internal static JObject TString(string value) => value == null ? - TObject("string", is_null: true) : + JObject.FromObject(new { type = "object", className = "string", subtype = "null" }) : JObject.FromObject(new { type = "string", value = @value }); internal static JObject TNumber(int value) => @@ -951,6 +993,12 @@ internal static JObject TDelegate(string className, string target) => JObject.Fr internal static JObject TIgnore() => JObject.FromObject(new { __custom_type = "ignore_me" }); internal static JObject TGetter(string type) => JObject.FromObject(new { __custom_type = "getter", type_name = type }); + + internal static JObject TDateTime(DateTime dt) => JObject.FromObject(new + { + __custom_type = "datetime", + binary = dt.ToBinary() + }); } class DebugTestContext diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs index 34dc5ef605de1b..16296b6f033d1d 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs @@ -235,7 +235,6 @@ await EvaluateAndCheck( Assert.Equal("IntAdd", scope["name"]); Assert.Equal("object", scope["object"]["type"]); - Assert.Equal("dotnet:scope:0", scope["object"]["objectId"]); CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 8, 4, scripts, scope["startLocation"]); CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 14, 4, scripts, scope["endLocation"]); return Task.CompletedTask; @@ -359,6 +358,78 @@ await CheckInspectLocalsAtBreakpointSite( } ); + [Fact] + public async Task InspectSimpleStringLocals() => + await CheckInspectLocalsAtBreakpointSite( + "Math", "TestSimpleStrings", 13, "TestSimpleStrings", + "window.setTimeout(function() { invoke_static_method ('[debugger-test] Math:TestSimpleStrings')(); }, 1);", + wait_for_event_fn: async (pause_location) => + { + var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + + var str_null = TString(null); + var str_empty = TString(String.Empty); + var str_spaces = TString(" "); + var str_esc = TString("\\"); + + await CheckProps(locals, new + { + str_null, + str_empty, + str_spaces, + str_esc, + + strings = TArray("string[]", 4) + }, "locals"); + + var strings_arr = await GetObjectOnLocals(locals, "strings"); + await CheckProps(strings_arr, new[] + { + str_null, + str_empty, + str_spaces, + str_esc + }, "locals#strings"); + } + ); + + [Theory] + [InlineData("TestNullableLocal", false)] + [InlineData("TestNullableLocalAsync", true)] + public async Task InspectNullableLocals(string method_name, bool is_async) => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.NullableTests", + method_name, + 10, + is_async ? "MoveNext" : method_name, + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] DebuggerTests.NullableTests:{method_name}'); }}, 1);", + wait_for_event_fn: async (pause_location) => + { + var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + var dt = new DateTime(2310, 1, 2, 3, 4, 5); + await CheckProps(locals, new + { + n_int = TNumber(5), + n_int_null = TObject("System.Nullable", null), + + n_dt = TDateTime(dt), + n_dt_null = TObject("System.Nullable", null), + + n_gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), + n_gs_null = TObject("System.Nullable>", null), + }, "locals"); + + // check gs + + var n_gs = GetAndAssertObjectWithName(locals, "n_gs"); + var n_gs_props = await GetProperties(n_gs["value"]?["objectId"]?.Value ()); + await CheckProps(n_gs_props, new + { + List = TObject("System.Collections.Generic.List", is_null: true), + StringField = TString("n_gs#StringField"), + Options = TEnum ("DebuggerTests.Options", "None") + }, nameof(n_gs)); + }); + [Theory] [InlineData(false)] [InlineData(true)] @@ -384,12 +455,6 @@ await CheckInspectLocalsAtBreakpointSite( } ); - object TGenericStruct(string typearg, string stringField) => new - { - List = TObject($"System.Collections.Generic.List<{typearg}>"), - StringField = TString(stringField) - }; - [Fact] public async Task RuntimeGetPropertiesWithInvalidScopeIdTest() { @@ -416,7 +481,6 @@ await EvaluateAndCheck( var top_frame = pause_location["callFrames"][0]; var scope = top_frame["scopeChain"][0]; - Assert.Equal("dotnet:scope:0", scope["object"]["objectId"]); // Try to get an invalid scope! var get_prop_req = JObject.FromObject(new @@ -559,7 +623,7 @@ await insp.Ready(async (cli, token) => Assert.Equal(3, props.Count()); CheckNumber(props, "A", 10); CheckString(props, "B", "xx"); - CheckObject(props, "c", "object"); + CheckString(props, "c", "20_xx"); // Check UseComplex frame var locals_m1 = await GetLocalsForFrame(pause_location["callFrames"][3], debugger_test_loc, 23, 8, "UseComplex"); @@ -577,7 +641,7 @@ await insp.Ready(async (cli, token) => Assert.Equal(3, props.Count()); CheckNumber(props, "A", 10); CheckString(props, "B", "xx"); - CheckObject(props, "c", "object"); + CheckString(props, "c", "20_xx"); pause_location = await StepAndCheck(StepKind.Over, dep_cs_loc, 23, 8, "DoStuff", times: 2); // Check UseComplex frame again @@ -596,7 +660,7 @@ await insp.Ready(async (cli, token) => Assert.Equal(3, props.Count()); CheckNumber(props, "A", 10); CheckString(props, "B", "xx"); - CheckObject(props, "c", "object"); + CheckString(props, "c", "20_xx"); }); } @@ -641,12 +705,10 @@ await insp.Ready(async (cli, token) => var dt = new DateTime(2020, 1, 2, 3, 4, 5); await CheckProps(ss_props, new { - dt = TValueType("System.DateTime", dt.ToString()), + dt = TDateTime(dt), gs = TValueType("Math.GenericStruct") }, "ss_props"); - await CheckDateTime(ss_props, "dt", new DateTime(2020, 1, 2, 3, 4, 5)); - // Check OuterMethod frame var locals_m1 = await GetLocalsForFrame(wait_res["callFrames"][1], debugger_test_loc, 87, 8, "OuterMethod"); Assert.Equal(5, locals_m1.Count()); @@ -892,7 +954,7 @@ await insp.Ready(async (cli, token) => { V = TGetter("V"), str_member = TString("set in MethodWithLocalStructs#SimpleStruct#str_member"), - dt = TValueType("System.DateTime", dt.ToString()), + dt = TDateTime(dt), gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), Kind = TEnum("System.DateTimeKind", "Utc") }, "ss_local"); @@ -900,8 +962,6 @@ await insp.Ready(async (cli, token) => { var gres = await InvokeGetter(GetAndAssertObjectWithName(locals, "ss_local"), "V"); await CheckValue(gres.Value["result"], TNumber(0xDEADBEEF + 2), $"ss_local#V"); - // Check ss_local.dt - await CheckDateTime(ss_local_props, "dt", dt); // Check ss_local.gs var gs_props = await GetObjectOnLocals(ss_local_props, "gs"); @@ -929,18 +989,17 @@ await insp.Ready(async (cli, token) => foreach (var (name, bias, dt_kind) in exp) { dt = new DateTime(2020 + bias, 1 + bias, 2 + bias, 3 + bias, 5 + bias, 6 + bias); - var ssp_props = await CompareObjectPropertiesFor(vt_local_props, name, + await CompareObjectPropertiesFor(vt_local_props, name, new { V = TGetter("V"), str_member = TString($"{name}#string#0#SimpleStruct#str_member"), - dt = TValueType("System.DateTime", dt.ToString()), + dt = TDateTime(dt), gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), Kind = TEnum("System.DateTimeKind", dt_kind) }, label: $"vt_local_props.{name}"); - await CheckDateTime(ssp_props, "dt", dt); var gres = await InvokeGetter(GetAndAssertObjectWithName(vt_local_props, name), "V"); await CheckValue(gres.Value["result"], TNumber(0xDEADBEEF + (uint)dt.Month), $"{name}#V"); } @@ -949,6 +1008,110 @@ await insp.Ready(async (cli, token) => }); } + [Theory] + [InlineData("BoxingTest", false)] + [InlineData("BoxingTestAsync", true)] + public async Task InspectBoxedLocals(string method_name, bool is_async) => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTest", + method_name, + 17, + is_async ? "MoveNext" : method_name, + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] DebuggerTest:{method_name}'); }}, 1);", + wait_for_event_fn: async (pause_location) => + { + var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + var dt = new DateTime(2310, 1, 2, 3, 4, 5); + await CheckProps(locals, new + { + n_i = TNumber(5), + o_i = TNumber(5), + o_n_i = TNumber(5), + o_s = TString("foobar"), + o_obj = TObject("Math"), + + n_gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), + o_gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), + o_n_gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), + + n_dt = TDateTime(dt), + o_dt = TDateTime(dt), + o_n_dt = TDateTime(dt), + + o_null = TObject("object", is_null: true), + o_ia = TArray("int[]", 2), + }, "locals"); + + foreach (var name in new[] { "n_gs", "o_gs", "o_n_gs" }) + { + var gs = GetAndAssertObjectWithName(locals, name); + var gs_props = await GetProperties(gs["value"]?["objectId"]?.Value ()); + await CheckProps(gs_props, new + { + List = TObject("System.Collections.Generic.List", is_null: true), + StringField = TString("n_gs#StringField"), + Options = TEnum ("DebuggerTests.Options", "None") + }, name); + } + + var o_ia_props = await GetObjectOnLocals(locals, "o_ia"); + await CheckProps(o_ia_props, new[] + { + TNumber(918), + TNumber(58971) + }, nameof(o_ia_props)); + }); + + [Theory] + [InlineData("BoxedTypeObjectTest", false)] + [InlineData("BoxedTypeObjectTestAsync", true)] + public async Task InspectBoxedTypeObject(string method_name, bool is_async) => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTest", + method_name, + 9, + is_async ? "MoveNext" : method_name, + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] DebuggerTest:{method_name}'); }}, 1);", + wait_for_event_fn: async (pause_location) => + { + var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + var dt = new DateTime(2310, 1, 2, 3, 4, 5); + await CheckProps(locals, new + { + i = TNumber(5), + o0 = TNumber(5), + o1 = TNumber(5), + o2 = TNumber(5), + o3 = TNumber(5), + + oo = TObject("object"), + oo0 = TObject("object"), + }, "locals"); + }); + + [Theory] + [InlineData("BoxedAsClass", false)] + [InlineData("BoxedAsClassAsync", true)] + public async Task InspectBoxedAsClassLocals(string method_name, bool is_async) => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTest", + method_name, + 6, + is_async ? "MoveNext" : method_name, + $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] DebuggerTest:{method_name}'); }}, 1);", + wait_for_event_fn: async (pause_location) => + { + var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + var dt = new DateTime(2310, 1, 2, 3, 4, 5); + Console.WriteLine (locals); + + await CheckProps(locals, new + { + vt_dt = TDateTime(new DateTime(4819, 5, 6, 7, 8, 9)), + vt_gs = TValueType("Math.GenericStruct"), + e = TEnum("System.IO.FileMode", "0"), + ee = TEnum("System.IO.FileMode", "Append") + }, "locals"); + }); + + [Theory] [InlineData(false)] [InlineData(true)] @@ -983,7 +1146,7 @@ await insp.Ready(async (cli, token) => { V = TGetter("V"), str_member = TString("ss_local#SimpleStruct#string#0#SimpleStruct#str_member"), - dt = TValueType("System.DateTime", dt.ToString()), + dt = TDateTime(dt), gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), Kind = TEnum("System.DateTimeKind", "Local") }; @@ -1002,9 +1165,6 @@ await insp.Ready(async (cli, token) => await CheckValue(res.Value["result"], TNumber(0xDEADBEEF + (uint)dt.Month), "ss_arg#V"); { - // Check ss_local.dt - await CheckDateTime(ss_arg_props, "dt", dt); - // Check ss_local.gs await CompareObjectPropertiesFor(ss_arg_props, "gs", ss_local_gs); } @@ -1024,7 +1184,7 @@ await insp.Ready(async (cli, token) => { V = TGetter("V"), str_member = TString("ValueTypesTest#MethodWithStructArgs#updated#ss_arg#str_member"), - dt = TValueType("System.DateTime", dt.ToString()), + dt = TDateTime(dt), gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), Kind = TEnum("System.DateTimeKind", "Utc") }; @@ -1043,8 +1203,6 @@ await insp.Ready(async (cli, token) => List = TObject("System.Collections.Generic.List"), Options = TEnum("DebuggerTests.Options", "Option1") }); - - await CheckDateTime(ss_arg_props, "dt", dt); } // Check locals on previous frame, same as earlier in this test @@ -1126,20 +1284,16 @@ async Task CheckLocals(JToken pause_location, DateTime obj_dt, DateTime vt_dt) { await CheckProps(obj_props, new { - DT = TValueType("System.DateTime", obj_dt.ToString()) + DT = TDateTime(obj_dt) }, "locals#obj.DT", num_fields: 5); - - await CheckDateTime(obj_props, "DT", obj_dt); } var vt_props = await GetObjectOnLocals(locals, "vt"); { await CheckProps(vt_props, new { - DT = TValueType("System.DateTime", vt_dt.ToString()) + DT = TDateTime(vt_dt) }, "locals#obj.DT", num_fields: 5); - - await CheckDateTime(vt_props, "DT", vt_dt); } } } @@ -1218,10 +1372,8 @@ async Task CheckArrayElements(JToken pause_location, DateTime dt) var sst0 = await GetObjectOnLocals(ssta, "0"); await CheckProps(sst0, new { - DT = TValueType("System.DateTime", dt.ToString()) + DT = TDateTime(dt) }, "dta [0]", num_fields: 5); - - await CheckDateTime(sst0, "DT", dt); } } @@ -1265,7 +1417,7 @@ await insp.Ready(async (cli, token) => { V = TGetter("V"), str_member = TString("set in MethodWithLocalStructsStaticAsync#SimpleStruct#str_member"), - dt = TValueType("System.DateTime", dt.ToString()), + dt = TDateTime(dt), gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), Kind = TEnum("System.DateTimeKind", "Utc") }, "ss_local"); @@ -1274,9 +1426,6 @@ await insp.Ready(async (cli, token) => var gres = await InvokeGetter(GetAndAssertObjectWithName(locals, "ss_local"), "V"); await CheckValue(gres.Value["result"], TNumber(0xDEADBEEF + 2), $"ss_local#V"); - // Check ss_local.dt - await CheckDateTime(ss_local_props, "dt", dt); - // Check ss_local.gs await CompareObjectPropertiesFor(ss_local_props, "gs", new @@ -1340,8 +1489,8 @@ await insp.Ready(async (cli, token) => await CheckProps(frame_locals, new { call_other = TBool(call_other), - dt0 = TValueType("System.DateTime", dt0.ToString()), - dt1 = TValueType("System.DateTime", dt1.ToString()), + dt0 = TDateTime(dt0), + dt1 = TDateTime(dt1), dto = TValueType("System.DateTimeOffset", dto.ToString()), ts = TValueType("System.TimeSpan", ts.ToString()), dec = TValueType("System.Decimal", "123987123"), @@ -1378,10 +1527,10 @@ await CompareObjectPropertiesFor(frame_locals, "dto", var DT = new DateTime(2004, 10, 15, 1, 2, 3); var DTO = new DateTimeOffset(dt0, new TimeSpan(2, 14, 0)); - var obj_props = await CompareObjectPropertiesFor(frame_locals, "obj", + await CompareObjectPropertiesFor(frame_locals, "obj", new { - DT = TValueType("System.DateTime", DT.ToString()), + DT = TDateTime(DT), DTO = TValueType("System.DateTimeOffset", DTO.ToString()), TS = TValueType("System.TimeSpan", ts.ToString()), Dec = TValueType("System.Decimal", "1239871"), @@ -1392,7 +1541,7 @@ await CompareObjectPropertiesFor(frame_locals, "dto", var sst_props = await CompareObjectPropertiesFor(frame_locals, "sst", new { - DT = TValueType("System.DateTime", DT.ToString()), + DT = TDateTime(DT), DTO = TValueType("System.DateTimeOffset", DTO.ToString()), TS = TValueType("System.TimeSpan", ts.ToString()), Dec = TValueType("System.Decimal", "1239871"), @@ -1523,6 +1672,184 @@ async Task _invoke_getter(string obj_id, string property_name, bool expe } } + [Fact] + public async Task CreateGoodBreakpointAndHitAndRemoveAndDontHit() + { + var insp = new Inspector(); + + //Collect events + var scripts = SubscribeToScripts(insp); + + await Ready(); + await insp.Ready(async (cli, token) => + { + ctx = new DebugTestContext(cli, insp, token, scripts); + + var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8); + var bp2 = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 12, 8); + var pause_location = await EvaluateAndCheck( + "window.setTimeout(function() { invoke_add(); invoke_add()}, 1);", + "dotnet://debugger-test.dll/debugger-test.cs", 10, 8, + "IntAdd"); + + Assert.Equal("other", pause_location["reason"]?.Value()); + Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"]?[0]?.Value()); + + await RemoveBreakpoint(bp.Value["breakpointId"]?.ToString()); + await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", "dotnet://debugger-test.dll/debugger-test.cs", 12, 8, "IntAdd"); + await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", "dotnet://debugger-test.dll/debugger-test.cs", 12, 8, "IntAdd"); + }); + } + + [Fact] + public async Task CreateGoodBreakpointAndHitAndRemoveTwice() + { + var insp = new Inspector(); + + //Collect events + var scripts = SubscribeToScripts(insp); + + await Ready(); + await insp.Ready(async (cli, token) => + { + ctx = new DebugTestContext(cli, insp, token, scripts); + + var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8); + var bp2 = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 12, 8); + var pause_location = await EvaluateAndCheck( + "window.setTimeout(function() { invoke_add(); invoke_add()}, 1);", + "dotnet://debugger-test.dll/debugger-test.cs", 10, 8, + "IntAdd"); + + Assert.Equal("other", pause_location["reason"]?.Value()); + Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"]?[0]?.Value()); + + await RemoveBreakpoint(bp.Value["breakpointId"]?.ToString()); + await RemoveBreakpoint(bp.Value["breakpointId"]?.ToString()); + }); + } + + [Fact] + public async Task CreateGoodBreakpointAndHitAndRemoveAndDontHitAndCreateAgainAndHit() + { + var insp = new Inspector(); + + //Collect events + var scripts = SubscribeToScripts(insp); + + await Ready(); + await insp.Ready(async (cli, token) => + { + ctx = new DebugTestContext(cli, insp, token, scripts); + + var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8); + var bp2 = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 12, 8); + var pause_location = await EvaluateAndCheck( + "window.setTimeout(function() { invoke_add(); invoke_add(); invoke_add(); invoke_add()}, 1);", + "dotnet://debugger-test.dll/debugger-test.cs", 10, 8, + "IntAdd"); + + Assert.Equal("other", pause_location["reason"]?.Value()); + Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"]?[0]?.Value()); + + await RemoveBreakpoint(bp.Value["breakpointId"]?.ToString()); + await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", "dotnet://debugger-test.dll/debugger-test.cs", 12, 8, "IntAdd"); + await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", "dotnet://debugger-test.dll/debugger-test.cs", 12, 8, "IntAdd"); + bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8); + await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", "dotnet://debugger-test.dll/debugger-test.cs", 10, 8, "IntAdd"); + + }); + } + + [Fact] + public async Task MulticastDelegateTest() => await CheckInspectLocalsAtBreakpointSite( + "MulticastDelegateTestClass", "Test", 5, "Test", + "window.setTimeout(function() { invoke_static_method('[debugger-test] MulticastDelegateTestClass:run'); })", + wait_for_event_fn: async (pause_location) => + { + var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + var this_props = await GetObjectOnLocals(frame_locals, "this"); + await CheckProps(this_props, new + { + TestEvent = TSymbol("System.EventHandler"), + Delegate = TSymbol("System.MulticastDelegate") + }, "this_props"); + }); + + [Theory] + [InlineData("EmptyClass", false)] + [InlineData("EmptyClass", true)] + [InlineData("EmptyStruct", false)] + [InlineData("EmptyStruct", true)] + public async Task EmptyTypeWithNoLocalsOrParams(string type_name, bool is_async) => await CheckInspectLocalsAtBreakpointSite( + type_name, + $"StaticMethodWithNoLocals{ (is_async ? "Async" : "") }", + 1, + is_async ? "MoveNext" : "StaticMethodWithNoLocals", + $"window.setTimeout(function() {{ invoke_static_method('[debugger-test] {type_name}:run'); }})", + wait_for_event_fn: async (pause_location) => + { + var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + AssertEqual(0, frame_locals.Values().Count(), "locals"); + }); + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task StaticMethodWithLocalEmptyStructThatWillGetExpanded(bool is_async) => await CheckInspectLocalsAtBreakpointSite( + "EmptyStruct", + $"StaticMethodWithLocalEmptyStruct{ (is_async ? "Async" : "") }", + 1, + is_async ? "MoveNext" : "StaticMethodWithLocalEmptyStruct", + $"window.setTimeout(function() {{ invoke_static_method('[debugger-test] EmptyStruct:run'); }})", + wait_for_event_fn: async (pause_location) => + { + var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value()); + await CheckProps(frame_locals, new + { + es = TValueType("EmptyStruct") + }, "locals"); + + var es = GetAndAssertObjectWithName(frame_locals, "es"); + var es_props = await GetProperties(es["value"]["objectId"]?.Value()); + AssertEqual(0, es_props.Values().Count(), "es_props"); + }); + + [Fact] + public async Task PreviousFrameForAReflectedCall() => await CheckInspectLocalsAtBreakpointSite( + "DebuggerTests.GetPropertiesTests.CloneableStruct", "SimpleStaticMethod", 1, "SimpleStaticMethod", + "window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.GetPropertiesTests.TestWithReflection:run'); })", + wait_for_event_fn: async (pause_location) => + { + var frame = FindFrame(pause_location, "InvokeReflectedStaticMethod"); + Assert.NotNull(frame); + + var frame_locals = await GetProperties(frame["callFrameId"].Value()); + + await CheckProps(frame_locals, new + { + mi = TObject("System.Reflection.MethodInfo"), + dt = TDateTime(new DateTime(4210, 3, 4, 5, 6, 7)), + i = TNumber(4), + strings = TArray("string[]", 1), + cs = TValueType("DebuggerTests.GetPropertiesTests.CloneableStruct"), + + num = TNumber(10), + name = TString("foobar"), + some_date = TDateTime(new DateTime(1234, 6, 7, 8, 9, 10)), + num1 = TNumber(100), + str2 = TString("xyz"), + num3 = TNumber(345), + str3 = TString("abc") + }, "InvokeReflectedStaticMethod#locals"); + }); + + JObject FindFrame(JObject pause_location, string function_name) + => pause_location["callFrames"] + ?.Values() + ?.Where(f => f["functionName"]?.Value() == function_name) + ?.FirstOrDefault(); + //TODO add tests covering basic stepping behavior as step in/out/over } } diff --git a/src/mono/wasm/debugger/tests/debugger-array-test.cs b/src/mono/wasm/debugger/tests/debugger-array-test.cs index 282db743e882f5..26acccf68dc767 100644 --- a/src/mono/wasm/debugger/tests/debugger-array-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-array-test.cs @@ -168,7 +168,7 @@ public static async Task ValueTypeLocalsAsync(bool call_other = false) var point = new Point { X = 45, Y = 51, Id = "point#Id", Color = RGB.Green }; Console.WriteLine($"point_arr: {point_arr.Length}, T: {t1}, point: {point}"); - return (t1, new Point[] { point_arr[0], point_arr[1], point }); + return await Task.FromResult((t1, new Point[] { point_arr[0], point_arr[1], point })); } // A workaround for method invocations on structs not working right now @@ -249,7 +249,7 @@ public async Task AsyncInstanceMethod(SimpleClass sc_arg) { var local_gs = new SimpleGenericStruct { Id = "local_gs#Id", Color = RGB.Green, Value = 4 }; sc_arg.Id = "sc_arg#Id"; - Console.WriteLine($"AsyncInstanceMethod sc_arg: {sc_arg.Id}, local_gs: {local_gs.Id}"); + Console.WriteLine($"AsyncInstanceMethod sc_arg: {sc_arg.Id}, local_gs: {local_gs.Id}"); await Task.CompletedTask; } public void GenericInstanceMethod(T sc_arg) where T : SimpleClass diff --git a/src/mono/wasm/debugger/tests/debugger-evaluate-test.cs b/src/mono/wasm/debugger/tests/debugger-evaluate-test.cs index ffb6fadafb18ef..498c02832e335f 100644 --- a/src/mono/wasm/debugger/tests/debugger-evaluate-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-evaluate-test.cs @@ -13,17 +13,18 @@ public class TestEvaluate public int b; public int c; public DateTime dt = new DateTime(2000, 5, 4, 3, 2, 1); - public void run(int g, int h, string valString) + public TestEvaluate NullIfAIsNotZero => a != 0 ? null : this; + public void run(int g, int h, string a, string valString, int this_a) { int d = g + 1; int e = g + 2; int f = g + 3; int i = d + e + f; var local_dt = new DateTime(2010, 9, 8, 7, 6, 5); - a = 1; + this.a = 1; b = 2; c = 3; - a = a + 1; + this.a = this.a + 1; b = b + 1; c = c + 1; } @@ -32,31 +33,103 @@ public void run(int g, int h, string valString) public static void EvaluateLocals() { TestEvaluate f = new TestEvaluate(); - f.run(100, 200, "test"); + f.run(100, 200, "9000", "test", 45); - var f_s = new EvaluateTestsStruct(); - f_s.EvaluateTestsStructInstanceMethod(100, 200, "test"); - f_s.GenericInstanceMethodOnStruct(100, 200, "test"); + var f_s = new EvaluateTestsStructWithProperties(); + f_s.InstanceMethod(100, 200, "test", f_s); + f_s.GenericInstanceMethod(100, 200, "test", f_s); var f_g_s = new EvaluateTestsGenericStruct(); f_g_s.EvaluateTestsGenericStructInstanceMethod(100, 200, "test"); - Console.WriteLine($"a: {f.a}, b: {f.b}, c: {f.c}"); } } - public struct EvaluateTestsStruct + public struct EvaluateTestsGenericStruct { public int a; public int b; public int c; DateTime dateTime; - public void EvaluateTestsStructInstanceMethod(int g, int h, string valString) + public void EvaluateTestsGenericStructInstanceMethod(int g, int h, string valString) { int d = g + 1; int e = g + 2; int f = g + 3; - int i = d + e + f; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); + a = 1; + b = 2; + c = 3; + dateTime = new DateTime(2020, 1, 2, 3, 4, 5); + T t = default(T); + a = a + 1; + b = b + 2; + c = c + 3; + } + } + + public class EvaluateTestsClassWithProperties + { + public int a; + public int b; + public int c { get; set; } + + public DateTime dateTime; + public DateTime DTProp => dateTime.AddMinutes(10); + public int IntProp => a + 5; + public string SetOnlyProp { set { a = value.Length; } } + public EvaluateTestsClassWithProperties NullIfAIsNotZero => a != 1908712 ? null : new EvaluateTestsClassWithProperties(0); + public EvaluateTestsClassWithProperties NewInstance => new EvaluateTestsClassWithProperties(3); + + public EvaluateTestsClassWithProperties(int bias) + { + a = 4; + b = 0; + c = 0; + dateTime = new DateTime(2010, 9, 8, 7, 6, 5 + bias); + } + + public static async Task run() + { + var obj = new EvaluateTestsClassWithProperties(0); + var obj2 = new EvaluateTestsClassWithProperties(0); + obj.InstanceMethod(400, 123, "just a test", obj2); + new EvaluateTestsClassWithProperties(0).GenericInstanceMethod(400, 123, "just a test", obj2); + new EvaluateTestsClassWithProperties(0).EvaluateShadow(new DateTime(2020, 3, 4, 5, 6, 7), obj.NewInstance); + + await new EvaluateTestsClassWithProperties(0).InstanceMethodAsync(400, 123, "just a test", obj2); + await new EvaluateTestsClassWithProperties(0).GenericInstanceMethodAsync(400, 123, "just a test", obj2); + await new EvaluateTestsClassWithProperties(0).EvaluateShadowAsync(new DateTime(2020, 3, 4, 5, 6, 7), obj.NewInstance); + } + + public void EvaluateShadow(DateTime dateTime, EvaluateTestsClassWithProperties me) + { + string a = "hello"; + Console.WriteLine($"Evaluate - break here"); + SomeMethod(dateTime, me); + } + + public async Task EvaluateShadowAsync(DateTime dateTime, EvaluateTestsClassWithProperties me) + { + string a = "hello"; + Console.WriteLine($"EvaluateShadowAsync - break here"); + await Task.CompletedTask; + } + + public void SomeMethod(DateTime me, EvaluateTestsClassWithProperties dateTime) + { + Console.WriteLine($"break here"); + + var DTProp = "hello"; + Console.WriteLine($"dtProp: {DTProp}"); + } + + public async Task InstanceMethodAsync(int g, int h, string valString, EvaluateTestsClassWithProperties me) + { + int d = g + 1; + int e = g + 2; + int f = g + 3; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); a = 1; b = 2; c = 3; @@ -64,45 +137,176 @@ public void EvaluateTestsStructInstanceMethod(int g, int h, string valString) a = a + 1; b = b + 1; c = c + 1; + await Task.CompletedTask; } - public void GenericInstanceMethodOnStruct(int g, int h, string valString) + public void InstanceMethod(int g, int h, string valString, EvaluateTestsClassWithProperties me) { int d = g + 1; int e = g + 2; int f = g + 3; - int i = d + e + f; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); a = 1; b = 2; c = 3; dateTime = new DateTime(2020, 1, 2, 3, 4, 5); + a = a + 1; + b = b + 1; + c = c + 1; + } + + public void GenericInstanceMethod(int g, int h, string valString, EvaluateTestsClassWithProperties me) + { + int d = g + 1; + int e = g + 2; + int f = g + 3; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); + a = 1; + b = 2; + c = 3; + dateTime = new DateTime(2020, 1, 2, 3, 4, 5); + a = a + 1; + b = b + 1; + c = c + 1; T t = default(T); + } + + public async Task GenericInstanceMethodAsync(int g, int h, string valString, EvaluateTestsClassWithProperties me) + { + int d = g + 1; + int e = g + 2; + int f = g + 3; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); + a = 1; + b = 2; + c = 3; + dateTime = new DateTime(2020, 1, 2, 3, 4, 5); a = a + 1; b = b + 1; c = c + 1; + T t = default(T); + return await Task.FromResult(default(T)); } } - public struct EvaluateTestsGenericStruct + public struct EvaluateTestsStructWithProperties { public int a; public int b; - public int c; - DateTime dateTime; - public void EvaluateTestsGenericStructInstanceMethod(int g, int h, string valString) + public int c { get; set; } + + public DateTime dateTime; + public DateTime DTProp => dateTime.AddMinutes(10); + public int IntProp => a + 5; + public string SetOnlyProp { set { a = value.Length; } } + public EvaluateTestsClassWithProperties NullIfAIsNotZero => a != 1908712 ? null : new EvaluateTestsClassWithProperties(0); + public EvaluateTestsStructWithProperties NewInstance => new EvaluateTestsStructWithProperties(3); + + public EvaluateTestsStructWithProperties(int bias) + { + a = 4; + b = 0; + c = 0; + dateTime = new DateTime(2010, 9, 8, 7, 6, 5 + bias); + } + + public static async Task run() + { + var obj = new EvaluateTestsStructWithProperties(0); + var obj2 = new EvaluateTestsStructWithProperties(0); + obj.InstanceMethod(400, 123, "just a test", obj2); + new EvaluateTestsStructWithProperties(0).GenericInstanceMethod(400, 123, "just a test", obj2); + new EvaluateTestsStructWithProperties(0).EvaluateShadow(new DateTime(2020, 3, 4, 5, 6, 7), obj.NewInstance); + + await new EvaluateTestsStructWithProperties(0).InstanceMethodAsync(400, 123, "just a test", obj2); + await new EvaluateTestsStructWithProperties(0).GenericInstanceMethodAsync(400, 123, "just a test", obj2); + await new EvaluateTestsStructWithProperties(0).EvaluateShadowAsync(new DateTime(2020, 3, 4, 5, 6, 7), obj.NewInstance); + } + + public void EvaluateShadow(DateTime dateTime, EvaluateTestsStructWithProperties me) + { + string a = "hello"; + Console.WriteLine($"Evaluate - break here"); + SomeMethod(dateTime, me); + } + + public async Task EvaluateShadowAsync(DateTime dateTime, EvaluateTestsStructWithProperties me) + { + string a = "hello"; + Console.WriteLine($"EvaluateShadowAsync - break here"); + await Task.CompletedTask; + } + + public void SomeMethod(DateTime me, EvaluateTestsStructWithProperties dateTime) + { + Console.WriteLine($"break here"); + + var DTProp = "hello"; + Console.WriteLine($"dtProp: {DTProp}"); + } + + public async Task InstanceMethodAsync(int g, int h, string valString, EvaluateTestsStructWithProperties me) + { + int d = g + 1; + int e = g + 2; + int f = g + 3; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); + a = 1; + b = 2; + c = 3; + dateTime = new DateTime(2020, 1, 2, 3, 4, 5); + a = a + 1; + b = b + 1; + c = c + 1; + await Task.CompletedTask; + } + + public void InstanceMethod(int g, int h, string valString, EvaluateTestsStructWithProperties me) { int d = g + 1; int e = g + 2; int f = g + 3; - int i = d + e + f; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); a = 1; b = 2; c = 3; dateTime = new DateTime(2020, 1, 2, 3, 4, 5); + a = a + 1; + b = b + 1; + c = c + 1; + } + + public void GenericInstanceMethod(int g, int h, string valString, EvaluateTestsStructWithProperties me) + { + int d = g + 1; + int e = g + 2; + int f = g + 3; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); + a = 1; + b = 2; + c = 3; + dateTime = new DateTime(2020, 1, 2, 3, 4, 5); + a = a + 1; + b = b + 1; + c = c + 1; T t = default(T); + } + + public async Task GenericInstanceMethodAsync(int g, int h, string valString, EvaluateTestsStructWithProperties me) + { + int d = g + 1; + int e = g + 2; + int f = g + 3; + var local_dt = new DateTime(2025, 3, 5, 7, 9, 11); + a = 1; + b = 2; + c = 3; + dateTime = new DateTime(2020, 1, 2, 3, 4, 5); a = a + 1; - b = b + 2; - c = c + 3; + b = b + 1; + c = c + 1; + T t = default(T); + return await Task.FromResult(default(T)); } } } diff --git a/src/mono/wasm/debugger/tests/debugger-get-properties-test.cs b/src/mono/wasm/debugger/tests/debugger-get-properties-test.cs new file mode 100644 index 00000000000000..d591a34ce140ae --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-get-properties-test.cs @@ -0,0 +1,235 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace DebuggerTests.GetPropertiesTests +{ + + public interface IFirstName + { + string FirstName { get; } + } + + public interface ILastName + { + string LastName { get; } + } + + public interface IName : IFirstName, ILastName + { } + + public class BaseBaseClass + { + public string BaseBase_MemberForOverride { get; set; } + } + + public class BaseClass : BaseBaseClass, IName + { + private string _base_name; + private DateTime _base_dateTime => new DateTime(2134, 5, 7, 1, 9, 2); + protected int base_num; + + public string Base_AutoStringProperty { get; set; } + public virtual DateTime DateTimeForOverride { get; set; } + public string Base_AutoStringPropertyForOverrideWithField { get; set; } + + public virtual string StringPropertyForOverrideWithAutoProperty => "base#StringPropertyForOverrideWithAutoProperty"; + public virtual string Base_GetterForOverrideWithField => "base#Base_GetterForOverrideWithField"; + public new string BaseBase_MemberForOverride => "Base#BaseBase_MemberForOverride"; + + public string this[string s] => s + "_hello"; + + public BaseClass() + { + _base_name = "private_name"; + base_num = 5; + Base_AutoStringProperty = "base#Base_AutoStringProperty"; + DateTimeForOverride = new DateTime(2250, 4, 5, 6, 7, 8); + //AutoStringPropertyForOverride = "base#AutoStringPropertyForOverride"; + } + + public string GetBaseName() => _base_name; + + public virtual string FirstName => "BaseClass#FirstName"; + public virtual string LastName => "BaseClass#LastName"; + } + + public class DerivedClass : BaseClass, ICloneable + { + // public string _base_name = "DerivedClass#_base_name"; + private string _stringField = "DerivedClass#_stringField"; + private DateTime _dateTime = new DateTime(2020, 7, 6, 5, 4, 3); + private DateTime _DTProp => new DateTime(2200, 5, 6, 7, 8, 9); + + public int a; + public DateTime DateTime => _DTProp.AddMinutes(10); + public string AutoStringProperty { get; set; } + public override string FirstName => "DerivedClass#FirstName"; + + // Overrides an auto-property with a getter + public override DateTime DateTimeForOverride => new DateTime(2190, 9, 7, 5, 3, 2); + public override string StringPropertyForOverrideWithAutoProperty { get; } + public new string Base_AutoStringPropertyForOverrideWithField = "DerivedClass#Base_AutoStringPropertyForOverrideWithField"; + public new string Base_GetterForOverrideWithField = "DerivedClass#Base_GetterForOverrideWithField"; + public new string BaseBase_MemberForOverride = "DerivedClass#BaseBase_MemberForOverride"; + + public int this[int i, string s] => i + 1 + s.Length; + + object ICloneable.Clone() + { + // not meant to be used! + return new DerivedClass(); + } + + public DerivedClass() + { + a = 4; + AutoStringProperty = "DerivedClass#AutoStringProperty"; + StringPropertyForOverrideWithAutoProperty = "DerivedClass#StringPropertyForOverrideWithAutoProperty"; + } + + public static void run() + { + new DerivedClass().InstanceMethod(); + new DerivedClass().InstanceMethodAsync().Wait(); + } + + public string GetStringField() => _stringField; + + public void InstanceMethod() + { + Console.WriteLine($"break here"); + } + + public async Task InstanceMethodAsync() + { + Console.WriteLine($"break here"); + await Task.CompletedTask; + } + } + + public struct CloneableStruct : ICloneable, IName + { + private string _stringField; + private DateTime _dateTime; + private DateTime _DTProp => new DateTime(2200, 5, 6, 7, 8, 9); + + public int a; + public DateTime DateTime => _DTProp.AddMinutes(10); + public string AutoStringProperty { get; set; } + public string FirstName => "CloneableStruct#FirstName"; + public string LastName => "CloneableStruct#LastName"; + public int this[int i] => i + 1; + + object ICloneable.Clone() + { + // not meant to be used! + return new CloneableStruct(0); + } + + public CloneableStruct(int bias) + { + a = 4; + _stringField = "CloneableStruct#_stringField"; + _dateTime = new DateTime(2020, 7, 6, 5, 4, 3 + bias); + AutoStringProperty = "CloneableStruct#AutoStringProperty"; + } + + public static void run() + { + new CloneableStruct(3).InstanceMethod(); + new CloneableStruct(3).InstanceMethodAsync().Wait(); + } + + public string GetStringField() => _stringField; + + public void InstanceMethod() + { + Console.WriteLine($"break here"); + } + + public async Task InstanceMethodAsync() + { + Console.WriteLine($"break here"); + await Task.CompletedTask; + } + + public static void SimpleStaticMethod(DateTime dateTimeArg, string stringArg) + { + Console.WriteLine($"break here"); + } + + } + + public struct NestedStruct + { + public CloneableStruct cloneableStruct; + + public NestedStruct(int bias) + { + cloneableStruct = new CloneableStruct(bias); + } + + public static void run() + { + TestNestedStructStatic(); + TestNestedStructStaticAsync().Wait(); + } + + public static void TestNestedStructStatic() + { + var ns = new NestedStruct(3); + Console.WriteLine($"break here"); + } + + public static async Task TestNestedStructStaticAsync() + { + var ns = new NestedStruct(3); + Console.WriteLine($"break here"); + await Task.CompletedTask; + } + } + + class BaseClassForJSTest + { + public string kind = "car"; + public string make = "mini"; + public bool available => true; + } + + class DerivedClassForJSTest : BaseClassForJSTest + { + public string owner_name = "foo"; + public string owner_last_name => "bar"; + + public static void run() + { + var obj = new DerivedClassForJSTest(); + Console.WriteLine($"break here"); + } + } + + public class TestWithReflection + { + public static void run() + { + InvokeReflectedStaticMethod(10, "foobar", new DateTime(1234, 6, 7, 8, 9, 10), 100, "xyz", 345, "abc"); + } + + public static void InvokeReflectedStaticMethod(int num, string name, DateTime some_date, int num1, string str2, int num3, string str3) + { + var mi = typeof(CloneableStruct).GetMethod("SimpleStaticMethod"); + var dt = new DateTime(4210, 3, 4, 5, 6, 7); + int i = 4; + + string[] strings = new[] { "abc" }; + CloneableStruct cs = new CloneableStruct(); + + // var cs = new CloneableStruct(); + mi.Invoke(null, new object[] { dt, "called from run" }); + } + } +} diff --git a/src/mono/wasm/debugger/tests/debugger-nullable-test.cs b/src/mono/wasm/debugger/tests/debugger-nullable-test.cs new file mode 100644 index 00000000000000..9ef65e0e903549 --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-nullable-test.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +#nullable enable + +namespace DebuggerTests +{ + public class NullableTests + { + public static void TestNullableLocal() + { + int? n_int = 5; + int? n_int_null = null; + + DateTime? n_dt = new DateTime(2310, 1, 2, 3, 4, 5); + DateTime? n_dt_null = null; + + ValueTypesTest.GenericStruct? n_gs = new ValueTypesTest.GenericStruct { StringField = "n_gs#StringField" }; + ValueTypesTest.GenericStruct? n_gs_null = null; + + Console.WriteLine($"break here"); + } + + public static async Task TestNullableLocalAsync() + { + int? n_int = 5; + int? n_int_null = null; + + DateTime? n_dt = new DateTime(2310, 1, 2, 3, 4, 5); + DateTime? n_dt_null = null; + + ValueTypesTest.GenericStruct? n_gs = new ValueTypesTest.GenericStruct { StringField = "n_gs#StringField" }; + ValueTypesTest.GenericStruct? n_gs_null = null; + + Console.WriteLine($"break here"); + await Task.CompletedTask; + } + } +} diff --git a/src/mono/wasm/debugger/tests/debugger-pointers-test.cs b/src/mono/wasm/debugger/tests/debugger-pointers-test.cs index a82973e5ac37fe..e5ed9d65f9c004 100644 --- a/src/mono/wasm/debugger/tests/debugger-pointers-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-pointers-test.cs @@ -57,7 +57,7 @@ static unsafe void PointersAsArgsTest(int* ip, int** ipp, int*[] ipa, int**[] ip Console.WriteLine($"done!"); } - public static unsafe async Task LocalPointersAsync() + public static unsafe Task LocalPointersAsync() { int ivalue0 = 5; int ivalue1 = 10; @@ -91,7 +91,7 @@ public static unsafe async Task LocalPointersAsync() var cwp = new GenericClassWithPointers { Ptr = dtp }; var cwp_null = new GenericClassWithPointers(); - Console.WriteLine($"{(int)*ip}, {(int)**ipp}, {ipp_null == null}, {ip_null == null}, {ippa == null}, {ipa}, {(char)*cp}, {(vp == null ? "null" : "not null")}, {dtp->Second}, {gsp->IntField}, {cwp}, {cwp_null}, {gs_null}"); + Console.WriteLine($"{(int)*ip}, {(int)**ipp}, {ipp_null == null}, {ip_null == null}, {ippa == null}, {ipa}, {(char)*cp}, {(vp == null ? "null" : "not null")}, {dtp->Second}, {gsp->IntField}, {cwp}, {cwp_null}, {gs_null}"); return Task.CompletedTask; } // async methods cannot have unsafe params, so no test for that diff --git a/src/mono/wasm/debugger/tests/debugger-test.cs b/src/mono/wasm/debugger/tests/debugger-test.cs index 9b53cea250ce0d..ceb550001694b3 100644 --- a/src/mono/wasm/debugger/tests/debugger-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-test.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; - +using System.Threading.Tasks; public partial class Math { //Only append content to this class as the test suite depends on line info public static int IntAdd(int a, int b) @@ -285,7 +285,7 @@ async System.Threading.Tasks.Task MethodWithDelegatesAsync() public delegate void DelegateWithVoidReturn(GenericStruct gs); public static void DelegateTargetWithVoidReturn(GenericStruct gs) { } - delegate GenericStruct DelegateForSignatureTest(Math m, GenericStruct> gs); + public delegate GenericStruct DelegateForSignatureTest(Math m, GenericStruct> gs); static bool DelegateTargetForNestedFunc(T arg) => true; public struct SimpleStruct @@ -302,6 +302,23 @@ public struct GenericStruct public static GenericStruct DelegateTargetForSignatureTest(Math m, GenericStruct> gs) => new GenericStruct(); } + public static void TestSimpleStrings() + { + string str_null = null; + string str_empty = String.Empty; + string str_spaces = " "; + string str_esc = "\\"; + + var strings = new[] + { + str_null, + str_empty, + str_spaces, + str_esc + }; + Console.WriteLine($"break here"); + } + } public class DebuggerTest @@ -322,4 +339,182 @@ public static int locals() } static void locals_inner() { } + + public static void BoxingTest() + { + int? n_i = 5; + object o_i = n_i.Value; + object o_n_i = n_i; + + object o_s = "foobar"; + object o_obj = new Math(); + DebuggerTests.ValueTypesTest.GenericStruct? n_gs = new DebuggerTests.ValueTypesTest.GenericStruct { StringField = "n_gs#StringField" }; + object o_gs = n_gs.Value; + object o_n_gs = n_gs; + + DateTime? n_dt = new DateTime(2310, 1, 2, 3, 4, 5); + object o_dt = n_dt.Value; + object o_n_dt = n_dt; + object o_null = null; + object o_ia = new int[] {918, 58971}; + + Console.WriteLine ($"break here"); + } + + public static async Task BoxingTestAsync() + { + int? n_i = 5; + object o_i = n_i.Value; + object o_n_i = n_i; + + object o_s = "foobar"; + object o_obj = new Math(); + DebuggerTests.ValueTypesTest.GenericStruct? n_gs = new DebuggerTests.ValueTypesTest.GenericStruct { StringField = "n_gs#StringField" }; + object o_gs = n_gs.Value; + object o_n_gs = n_gs; + + DateTime? n_dt = new DateTime(2310, 1, 2, 3, 4, 5); + object o_dt = n_dt.Value; + object o_n_dt = n_dt; + object o_null = null; + object o_ia = new int[] {918, 58971}; + + Console.WriteLine ($"break here"); + await Task.CompletedTask; + } + + public static void BoxedTypeObjectTest() + { + int i = 5; + object o0 = i; + object o1 = o0; + object o2 = o1; + object o3 = o2; + + object oo = new object(); + object oo0 = oo; + Console.WriteLine ($"break here"); + } + public static async Task BoxedTypeObjectTestAsync() + { + int i = 5; + object o0 = i; + object o1 = o0; + object o2 = o1; + object o3 = o2; + + object oo = new object(); + object oo0 = oo; + Console.WriteLine ($"break here"); + await Task.CompletedTask; + } + + public static void BoxedAsClass() + { + ValueType vt_dt = new DateTime(4819, 5, 6, 7, 8, 9); + ValueType vt_gs = new Math.GenericStruct { StringField = "vt_gs#StringField" }; + Enum e = new System.IO.FileMode(); + Enum ee = System.IO.FileMode.Append; + + Console.WriteLine ($"break here"); + } + + public static async Task BoxedAsClassAsync() + { + ValueType vt_dt = new DateTime(4819, 5, 6, 7, 8, 9); + ValueType vt_gs = new Math.GenericStruct { StringField = "vt_gs#StringField" }; + Enum e = new System.IO.FileMode(); + Enum ee = System.IO.FileMode.Append; + + Console.WriteLine ($"break here"); + await Task.CompletedTask; + } +} + +public class MulticastDelegateTestClass +{ + event EventHandler TestEvent; + MulticastDelegate Delegate; + + public static void run() + { + var obj = new MulticastDelegateTestClass(); + obj.Test(); + obj.TestAsync().Wait(); + } + + public void Test() + { + TestEvent += (_, s) => Console.WriteLine(s); + TestEvent += (_, s) => Console.WriteLine(s + "qwe"); + Delegate = TestEvent; + + TestEvent?.Invoke(this, Delegate?.ToString()); + } + + public async Task TestAsync() + { + TestEvent += (_, s) => Console.WriteLine(s); + TestEvent += (_, s) => Console.WriteLine(s + "qwe"); + Delegate = TestEvent; + + TestEvent?.Invoke(this, Delegate?.ToString()); + await Task.CompletedTask; + } +} + +public class EmptyClass +{ + public static void StaticMethodWithNoLocals() + { + Console.WriteLine($"break here"); + } + + public static async Task StaticMethodWithNoLocalsAsync() + { + Console.WriteLine($"break here"); + await Task.CompletedTask; + } + + public static void run() + { + StaticMethodWithNoLocals(); + StaticMethodWithNoLocalsAsync().Wait(); + } +} + +public struct EmptyStruct +{ + public static void StaticMethodWithNoLocals() + { + Console.WriteLine($"break here"); + } + + public static async Task StaticMethodWithNoLocalsAsync() + { + Console.WriteLine($"break here"); + await Task.CompletedTask; + } + + public static void StaticMethodWithLocalEmptyStruct() + { + var es = new EmptyStruct(); + Console.WriteLine($"break here"); + } + + public static async Task StaticMethodWithLocalEmptyStructAsync() + { + var es = new EmptyStruct(); + Console.WriteLine($"break here"); + await Task.CompletedTask; + } + + public static void run() + { + StaticMethodWithNoLocals(); + StaticMethodWithNoLocalsAsync().Wait(); + + StaticMethodWithLocalEmptyStruct(); + StaticMethodWithLocalEmptyStructAsync().Wait(); + } } diff --git a/src/mono/wasm/debugger/tests/debugger-test.csproj b/src/mono/wasm/debugger/tests/debugger-test.csproj index 27f295c0de4dc7..045cf4c35fe7d8 100644 --- a/src/mono/wasm/debugger/tests/debugger-test.csproj +++ b/src/mono/wasm/debugger/tests/debugger-test.csproj @@ -10,6 +10,7 @@ true Library 219 + true portable @@ -27,7 +28,7 @@ Targets="Build;Publish"/> - diff --git a/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs b/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs index 0c5cc9019fb52b..d1330e0742e90b 100644 --- a/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs +++ b/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs @@ -11,7 +11,7 @@ public class ValueTypesTest public static void MethodWithLocalStructs() { var ss_local = new SimpleStruct("set in MethodWithLocalStructs", 1, DateTimeKind.Utc); - var gs_local = new GenericStruct { StringField = "gs_local#GenericStruct#StringField" }; + var gs_local = new GenericStruct { StringField = $"gs_local#GenericStruct#StringField" }; ValueTypesTest vt_local = new ValueTypesTest { @@ -190,7 +190,7 @@ static async Task MethodWithArgumentsForToStringTestAsync( DateTime dt0, DateTime dt1, TimeSpan ts, DateTimeOffset dto, decimal dec, Guid guid, DateTime[] dts, ClassForToStringTests obj, StructForToStringTests sst) { - Console.WriteLine($"MethodWithArgumentsForToStringTest: {dt0}, {dt1}, {ts}, {dec}, {guid}, {dts[0]}, {obj.DT}, {sst.DT}"); + Console.WriteLine($"MethodWithArgumentsForToStringTest: {dt0}, {dt1}, {ts}, {dec}, {guid}, {dts[0]}, {obj.DT}, {sst.DT}"); await Task.CompletedTask; } public static void MethodUpdatingValueTypeMembers() @@ -214,7 +214,7 @@ public static async Task MethodUpdatingValueTypeLocalsAsync() var dt = new DateTime(1, 2, 3, 4, 5, 6); Console.WriteLine($"#1"); dt = new DateTime(9, 8, 7, 6, 5, 4); - Console.WriteLine($"#2"); + Console.WriteLine($"#2"); await Task.CompletedTask; } public static void MethodUpdatingVTArrayMembers() diff --git a/src/mono/wasm/debugger/tests/other.js b/src/mono/wasm/debugger/tests/other.js index 93532686914e8e..c32eaa064b033f 100644 --- a/src/mono/wasm/debugger/tests/other.js +++ b/src/mono/wasm/debugger/tests/other.js @@ -63,3 +63,33 @@ function negative_cfo_test (str_value = null) { console.log (`break here`); return ptd; } + +function eval_call_on_frame_test () { + let obj = { + a: 5, + b: "hello", + c: { + c_x: 1 + }, + }; + + let obj_undefined = undefined; + console.log(`break here`); +} + +function get_properties_test () { + let vehicle = { + kind: "car", + make: "mini", + get available () { return true; } + }; + + let obj = { + owner_name: "foo", + get owner_last_name () { return "bar"; }, + } + // obj.prototype.this_vehicle = vehicle; + Object.setPrototypeOf(obj, vehicle); + + console.log(`break here`); +} diff --git a/src/mono/wasm/runtime/library_mono.js b/src/mono/wasm/runtime/library_mono.js index 909de6790777b1..74b31274b18d4c 100644 --- a/src/mono/wasm/runtime/library_mono.js +++ b/src/mono/wasm/runtime/library_mono.js @@ -287,43 +287,108 @@ var MonoSupportLib = { var i = 0; while (i < var_list.length) { let o = var_list [i]; - const name = o.name; - if (name == null || name == undefined) { - i ++; - out_list.push (o); - continue; - } + const this_has_name = o.name !== undefined; + let next_has_value_or_get_set = false; if (i + 1 < var_list.length) { + const next = var_list [i+1]; + next_has_value_or_get_set = next.value !== undefined || next.get !== undefined || next.set !== undefined; + } + + if (!this_has_name) { + // insert the object as-is + // Eg. in case of locals, the names are added + // later + i ++; + } else if (next_has_value_or_get_set) { + // found a {name} followed by a {value/get} o = Object.assign (o, var_list [i + 1]); + i += 2; + } else { + // missing value/get, so add a placeholder one + o.value = { + type: "symbol", + value: "", + description: "" + }; + i ++; } out_list.push (o); - i += 2; } return out_list; }, - _filter_automatic_properties: function (props) { - let names_found = {}; - let final_var_list = []; + _filter_automatic_properties: function (props, accessors_only=false) { + // Note: members in @props, have derived class members, followed by + // those from parent classes - for (var i in props) { - var p = props [i]; - if (p.name in names_found) - continue; + // Note: Auto-properties have backing fields, named with a special suffix. + // @props here will have the backing field, *and* the getter. + // + // But we want to return only one name/value pair: + // [name of the auto-property] = value of the backing field - if (p.name.endsWith ("k__BackingField")) - p.name = p.name.replace ("k__BackingField", "") - .replace ('<', '') - .replace ('>', ''); + let getters = {}; + let all_fields_except_backing_fields = {}; + let backing_fields = {}; - names_found [p.name] = p.name; - final_var_list.push (p); - } + // Split props into the 3 groups - backing_fields, getters, and all_fields_except_backing_fields + props.forEach(p => { + if (p.name === undefined) { + console.debug(`Bug: Found a member with no name. Skipping it. p: ${JSON.stringify(p)}`); + return; + } + + if (p.name.endsWith('k__BackingField')) { + const auto_prop_name = p.name.replace ('k__BackingField', '') + .replace ('<', '') + .replace ('>', ''); + + // Only take the first one, as that is overriding others + if (!(auto_prop_name in backing_fields)) + backing_fields[auto_prop_name] = Object.assign(p, { name: auto_prop_name }); + + } else if (p.get !== undefined) { + // if p wasn't overridden by a getter or a field, + // from a more derived class + if (!(p.name in getters) && !(p.name in all_fields_except_backing_fields)) + getters[p.name] = p; + + } else if (!(p.name in all_fields_except_backing_fields)) { + all_fields_except_backing_fields[p.name] = p; + } + }); + + // Filter/merge backing fields, and getters + Object.values(backing_fields).forEach(backing_field => { + const auto_prop_name = backing_field.name; + const getter = getters[auto_prop_name]; + + if (getter === undefined) { + // backing field with no getter + // eg. when a field overrides/`new string foo=..` + // an autoproperty + return; + } + + if (auto_prop_name in all_fields_except_backing_fields) { + delete getters[auto_prop_name]; + } else if (getter.__args.owner_class === backing_field.__args.owner_class) { + // getter+backing_field are from the same class. + // Add the backing_field value as a field + all_fields_except_backing_fields[auto_prop_name] = backing_field; + + // .. and drop the auto-prop getter + delete getters[auto_prop_name]; + } + }); + + if (accessors_only) + return Object.values(getters); - return final_var_list; + return Object.values(all_fields_except_backing_fields).concat(Object.values(getters)); }, /** Given `dotnet:object:foo:bar`, @@ -361,27 +426,103 @@ var MonoSupportLib = { return res; }, + _resolve_member_by_name: function (base_object, base_name, expr_parts) { + if (base_object === undefined || base_object.value === undefined) + throw new Error(`Bug: base_object is undefined`); + + if (base_object.value.type === 'object' && base_object.value.subtype === 'null') + throw new ReferenceError(`Null reference: ${base_name} is null`); + + if (base_object.value.type !== 'object') + throw new ReferenceError(`'.' is only supported on non-primitive types. Failed on '${base_name}'`); + + if (expr_parts.length == 0) + throw new Error(`Invalid member access expression`);//FIXME: need the full expression here + + const root = expr_parts[0]; + const props = this.mono_wasm_get_details(base_object.value.objectId, {}); + let resObject = props.find(l => l.name == root); + if (resObject !== undefined) { + if (resObject.value === undefined && resObject.get !== undefined) + resObject = this._invoke_getter(base_object.value.objectId, root); + } + + if (resObject === undefined || expr_parts.length == 1) + return resObject; + else { + expr_parts.shift(); + return this._resolve_member_by_name(resObject, root, expr_parts); + } + }, + + mono_wasm_eval_member_access: function (scope, var_list, rootObjectId, expr) { + if (expr === undefined || expr.length == 0) + throw new Error(`expression argument required`); + + let parts = expr.split('.'); + if (parts.length == 0) + throw new Error(`Invalid member access expression: ${expr}`); + + const root = parts[0]; + + const locals = this.mono_wasm_get_variables(scope, var_list); + let rootObject = locals.find(l => l.name === root); + if (rootObject === undefined) { + // check `this` + const thisObject = locals.find(l => l.name == "this"); + if (thisObject === undefined) + throw new ReferenceError(`Could not find ${root} in locals, and no 'this' found.`); + + const thisProps = this.mono_wasm_get_details(thisObject.value.objectId, {}); + rootObject = thisProps.find(tp => tp.name == root); + if (rootObject === undefined) + throw new ReferenceError(`Could not find ${root} in locals, or in 'this'`); + + if (rootObject.value === undefined && rootObject.get !== undefined) + rootObject = this._invoke_getter(thisObject.value.objectId, root); + } + + parts.shift(); + + if (parts.length == 0) + return rootObject; + + if (rootObject === undefined || rootObject.value === undefined) + throw new Error(`Could not get a value for ${root}`); + + return this._resolve_member_by_name(rootObject, root, parts); + }, + /** * @param {WasmId} id * @returns {object[]} */ - _get_vt_properties: function (id) { - let entry = this._id_table [id.idStr]; - if (entry !== undefined && entry.members !== undefined) - return entry.members; - - if (!isNaN (id.o.containerId)) - this._get_object_properties (id.o.containerId, true); - else if (!isNaN (id.o.arrayId)) - this._get_array_values (id, Number (id.o.arrayIdx), 1, true); - else - throw new Error (`Invalid valuetype id (${id.idStr}). Can't get properties for it.`); + _get_vt_properties: function (id, args={}) { + let entry = this._get_id_props (id.idStr); + + if (entry === undefined || entry.members === undefined) { + if (!isNaN (id.o.containerId)) { + // We are expanding, so get *all* the members. + // Which ones to return based on @args, can be determined + // at the time of return + this._get_object_properties (id.o.containerId, { expandValueTypes: true }); + } else if (!isNaN (id.o.arrayId)) + this._get_array_values (id, Number (id.o.arrayIdx), 1, true); + else + throw new Error (`Invalid valuetype id (${id.idStr}). Can't get properties for it.`); + } + // Let's try again entry = this._get_id_props (id.idStr); - if (entry !== undefined && entry.members !== undefined) + + if (entry !== undefined && entry.members !== undefined) { + if (args.accessorPropertiesOnly === true) + return entry.accessors; + return entry.members; + } - throw new Error (`Unknown valuetype id: ${id.idStr}`); + throw new Error (`Unknown valuetype id: ${id.idStr}. Failed to get properties for it.`); }, /** @@ -455,17 +596,37 @@ var MonoSupportLib = { return res; }, + // Keep in sync with the flags in mini-wasm-debugger.c + _get_properties_args_to_gpflags: function (args) { + let gpflags =0; + /* + Disabled for now. Instead, we ask debugger.c to return + ~all~ the members, and then handle the filtering in mono.js . + + if (args.ownProperties) + gpflags |= 1; + if (args.accessorPropertiesOnly) + gpflags |= 2; + */ + if (args.expandValueTypes) + gpflags |= 4; + + return gpflags; + }, + /** * @param {number} idNum * @param {boolean} expandValueTypes * @returns {object} */ - _get_object_properties: function(idNum, expandValueTypes) { - let { res_ok, res } = this.mono_wasm_get_object_properties_info (idNum, expandValueTypes); + _get_object_properties: function(idNum, args={}) { + let gpflags = this._get_properties_args_to_gpflags (args); + + let { res_ok, res } = this.mono_wasm_get_object_properties_info (idNum, gpflags); if (!res_ok) throw new Error (`Failed to get properties for ${idNum}`); - res = MONO._filter_automatic_properties (res); + res = MONO._filter_automatic_properties (res, args.accessorPropertiesOnly === true); res = this._assign_vt_ids (res, v => ({ containerId: idNum, fieldOffset: v.fieldOffset })); res = this._post_process_details (res); @@ -483,7 +644,8 @@ var MonoSupportLib = { if (isNaN (id.o.arrayId) || isNaN (startIdx)) throw new Error (`Invalid array id: ${id.idStr}`); - let { res_ok, res } = this.mono_wasm_get_array_values_info (id.o.arrayId, startIdx, count, expandValueTypes); + let gpflags = this._get_properties_args_to_gpflags({ expandValueTypes }); + let { res_ok, res } = this.mono_wasm_get_array_values_info (id.o.arrayId, startIdx, count, gpflags); if (!res_ok) throw new Error (`Failed to get properties for array id ${id.idStr}`); @@ -505,6 +667,8 @@ var MonoSupportLib = { if (details.length > 0) this._extract_and_cache_value_types(details); + // remove __args added by add_properties_var + details.forEach(d => delete d.__args); return details; }, @@ -540,12 +704,20 @@ var MonoSupportLib = { if (value.type != "object" || value.isValueType != true || value.expanded != true) // undefined would also give us false continue; + if (value.members === undefined) { + // this could happen for valuetypes that maybe + // we were not able to describe, like `ref` parameters + // So, skip that + continue; + } + // Generate objectId for expanded valuetypes value.objectId = value.objectId || this._new_or_add_id_props ({ scheme: 'valuetype' }); this._extract_and_cache_value_types (value.members); - const new_props = Object.assign ({ members: value.members }, value.__extra_vt_props); + const accessors = value.members.filter(m => m.get !== undefined); + const new_props = Object.assign ({ members: value.members, accessors }, value.__extra_vt_props); this._new_or_add_id_props ({ objectId: value.objectId, props: new_props }); delete value.members; @@ -695,7 +867,7 @@ var MonoSupportLib = { return res; }, - mono_wasm_get_details: function (objectId, args) { + mono_wasm_get_details: function (objectId, args={}) { let id = this._parse_object_id (objectId, true); switch (id.scheme) { @@ -703,14 +875,15 @@ var MonoSupportLib = { if (isNaN (id.value)) throw new Error (`Invalid objectId: ${objectId}. Expected a numeric id.`); - return this._get_object_properties(id.value, false); + args.expandValueTypes = false; + return this._get_object_properties(id.value, args); } case "array": return this._get_array_values (id); case "valuetype": - return this._get_vt_properties(id); + return this._get_vt_properties(id, args); case "cfo_res": return this._get_cfo_res_details (objectId, args); @@ -940,8 +1113,8 @@ var MonoSupportLib = { this._call_function_res_cache = {}; this._c_fn_table = {}; - this._register_c_var_fn ('mono_wasm_get_object_properties', 'bool', [ 'number', 'bool' ]); - this._register_c_var_fn ('mono_wasm_get_array_values', 'bool', [ 'number', 'number', 'number', 'bool' ]); + this._register_c_var_fn ('mono_wasm_get_object_properties', 'bool', [ 'number', 'number' ]); + this._register_c_var_fn ('mono_wasm_get_array_values', 'bool', [ 'number', 'number', 'number', 'number' ]); this._register_c_var_fn ('mono_wasm_invoke_getter_on_object', 'bool', [ 'number', 'string' ]); this._register_c_var_fn ('mono_wasm_invoke_getter_on_value', 'bool', [ 'number', 'number', 'string' ]); this._register_c_var_fn ('mono_wasm_get_local_vars', 'bool', [ 'number', 'number', 'number']); @@ -1467,7 +1640,7 @@ var MonoSupportLib = { }, _mono_wasm_add_string_var: function(var_value) { - if (var_value == 0) { + if (var_value === 0) { MONO.mono_wasm_add_null_var ("string"); return; } @@ -1481,31 +1654,20 @@ var MonoSupportLib = { }); }, - _mono_wasm_add_getter_var: function(className, invokable) { + _mono_wasm_add_getter_var: function(className) { const fixed_class_name = MONO._mono_csharp_fixup_class_name (className); - if (invokable != 0) { - var name; - if (MONO.var_info.length > 0) - name = MONO.var_info [MONO.var_info.length - 1].name; - name = (name === undefined) ? "" : name; - - MONO.var_info.push({ - get: { - className: "Function", - description: `get ${name} () {}`, - type: "function", - } - }); - } else { - var value = `${fixed_class_name} { get; }`; - MONO.var_info.push({ - value: { - type: "symbol", - description: value, - value: value, - } - }); - } + var name; + if (MONO.var_info.length > 0) + name = MONO.var_info [MONO.var_info.length - 1].name; + name = (name === undefined) ? "" : name; + + MONO.var_info.push({ + get: { + className: "Function", + description: `get ${name} () {}`, + type: "function", + } + }); }, _mono_wasm_add_array_var: function(className, objectId, length) { @@ -1550,7 +1712,7 @@ var MonoSupportLib = { value: { type : "object", className : fixed_class_name, - description : (toString == 0 ? fixed_class_name: Module.UTF8ToString (toString)), + description : (toString === 0 ? fixed_class_name: Module.UTF8ToString (toString)), expanded : true, isValueType : true, __extra_vt_props: { klass: args.klass, value64: base64String }, @@ -1592,17 +1754,36 @@ var MonoSupportLib = { value: { type: "object", className: fixed_class_name, - description: (toString == 0 ? fixed_class_name : Module.UTF8ToString (toString)), + description: (toString === 0 ? fixed_class_name : Module.UTF8ToString (toString)), isValueType: true } }); }, + mono_wasm_add_properties_var: function (name, args) { + if (typeof args !== 'object') + args = { field_offset: args }; + + if (args.owner_class !== undefined && args.owner_class !== 0) + args.owner_class = Module.UTF8ToString(args.owner_class); + + let name_obj = { + name: Module.UTF8ToString (name), + fieldOffset: args.field_offset, + __args: args + }; + if (args.is_own) + name_obj.isOwn = true; + + MONO.var_info.push(name_obj); + }, mono_wasm_add_typed_value: function (type, str_value, value) { let type_str = type; if (typeof type != 'string') type_str = Module.UTF8ToString (type); + + if (str_value !== 0) str_value = Module.UTF8ToString (str_value); switch (type_str) { @@ -1645,7 +1826,7 @@ var MonoSupportLib = { break; case "getter": - MONO._mono_wasm_add_getter_var (str_value, value); + MONO._mono_wasm_add_getter_var (str_value); break; case "array": @@ -1688,6 +1869,20 @@ var MonoSupportLib = { } break; + case "symbol": { + if (typeof value === 'object' && value.isClassName) + str_value = MONO._mono_csharp_fixup_class_name (str_value); + + MONO.var_info.push ({ + value: { + type: "symbol", + value: str_value, + description: str_value + } + }); + } + break; + default: { const msg = `'${str_value}' ${value}`; @@ -1768,11 +1963,8 @@ var MonoSupportLib = { MONO.mono_wasm_add_typed_value (type, str_value, value); }, - mono_wasm_add_properties_var: function(name, field_offset) { - MONO.var_info.push({ - name: Module.UTF8ToString (name), - fieldOffset: field_offset - }); + mono_wasm_add_properties_var: function(name, args) { + MONO.mono_wasm_add_properties_var (name, args); }, mono_wasm_set_is_async_method: function(objectId) { @@ -1820,7 +2012,7 @@ var MonoSupportLib = { value: { type: "object", className: fixed_class_name, - description: (toString == 0 ? fixed_class_name : Module.UTF8ToString (toString)), + description: (toString === 0 ? fixed_class_name : Module.UTF8ToString (toString)), objectId: "dotnet:object:"+ objectId, } }); @@ -1851,14 +2043,15 @@ var MonoSupportLib = { var args_sig = parts.splice (1).join (', '); return `${ret_sig} ${method_name} (${args_sig})`; } - let tgt_sig; if (targetName != 0) tgt_sig = args_to_sig (Module.UTF8ToString (targetName)); const type_name = MONO._mono_csharp_fixup_class_name (Module.UTF8ToString (className)); + if (tgt_sig === undefined) + tgt_sig = type_name; - if (objectId == -1) { + if (objectId == -1 || targetName === 0) { // Target property MONO.var_info.push ({ value: { @@ -1879,14 +2072,15 @@ var MonoSupportLib = { } }, - mono_wasm_add_frame: function(il, method, assembly_name, method_full_name) { + mono_wasm_add_frame: function(il, method, frame_id, assembly_name, method_full_name) { var parts = Module.UTF8ToString (method_full_name).split (":", 2); MONO.active_frames.push( { il_pos: il, method_token: method, assembly_name: Module.UTF8ToString (assembly_name), // Extract just the method name from `{class_name}:{method_name}` - method_name: parts [parts.length - 1] + method_name: parts [parts.length - 1], + frame_id }); },