diff --git a/CHANGELOG.md b/CHANGELOG.md index 3494f6f76..552a3e5ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Fixes**: +- Fix scope data loss from shared `sentry_value_t` containers while significantly improving scope merge performance with copy-on-write cloning. ([#1794](https://github.com/getsentry/sentry-native/pull/1794)) - Crashpad: wait reliably for crash report uploads. ([#1885](https://github.com/getsentry/sentry-native/pull/1885)) ## 0.15.4 diff --git a/src/sentry_scope.c b/src/sentry_scope.c index cbe6a4a78..22641f456 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -647,10 +647,10 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, sentry_value_new_string(options->run->installation_id)); } } else if (sentry_value_get_length(scope->user) > 0) { - PLACE_VALUE("user", scope->user); + PLACE_CLONED_VALUE("user", scope->user); } } - PLACE_VALUE("fingerprint", scope->fingerprint); + PLACE_CLONED_VALUE("fingerprint", scope->fingerprint); PLACE_STRING("transaction", scope->transaction); PLACE_VALUE("sdk", scope->client_sdk); diff --git a/src/sentry_value.c b/src/sentry_value.c index f27bfffda..11d74481f 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -93,6 +93,7 @@ typedef struct { sentry_value_t *items; size_t len; size_t allocated; + long refcount; } list_t; typedef struct { @@ -104,6 +105,7 @@ typedef struct { obj_pair_t *pairs; size_t len; size_t allocated; + long refcount; } obj_t; static const char * @@ -161,33 +163,162 @@ thing_get_type(const thing_t *thing) } static void -thing_free(thing_t *thing) +list_free(list_t *list) +{ + if (sentry__atomic_fetch_and_add(&list->refcount, -1) != 1) { + return; + } + for (size_t i = 0; i < list->len; i++) { + sentry_value_decref(list->items[i]); + } + sentry_free(list->items); + sentry_free(list); +} + +static void +obj_free(obj_t *obj) +{ + if (sentry__atomic_fetch_and_add(&obj->refcount, -1) != 1) { + return; + } + for (size_t i = 0; i < obj->len; i++) { + sentry_free(obj->pairs[i].k); + sentry_value_decref(obj->pairs[i].v); + } + sentry_free(obj->pairs); + sentry_free(obj); +} + +static list_t * +list_clone(const list_t *list) +{ + list_t *clone = SENTRY_MAKE(list_t); + if (!clone) { + return NULL; + } + clone->len = list->len; + clone->allocated = list->len; + clone->refcount = 1; + if (list->len) { + clone->items = sentry_malloc(sizeof(sentry_value_t) * list->len); + if (!clone->items) { + sentry_free(clone); + return NULL; + } + memcpy(clone->items, list->items, sizeof(sentry_value_t) * list->len); + for (size_t i = 0; i < list->len; i++) { + sentry_value_incref(clone->items[i]); + } + } + return clone; +} + +static obj_t * +obj_clone(const obj_t *obj) +{ + obj_t *clone = SENTRY_MAKE(obj_t); + if (!clone) { + return NULL; + } + clone->allocated = obj->len; + clone->refcount = 1; + if (obj->len) { + clone->pairs = sentry_malloc(sizeof(obj_pair_t) * obj->len); + if (!clone->pairs) { + sentry_free(clone); + return NULL; + } + for (size_t i = 0; i < obj->len; i++) { + clone->pairs[i].k = sentry__string_clone(obj->pairs[i].k); + if (!clone->pairs[i].k) { + obj_free(clone); + return NULL; + } + clone->pairs[i].v = obj->pairs[i].v; + sentry_value_incref(clone->pairs[i].v); + clone->len++; + } + } + return clone; +} + +static bool +thing_detach(thing_t *thing) { switch (thing_get_type(thing)) { case THING_TYPE_LIST: { - list_t *list = thing->payload._ptr; - for (size_t i = 0; i < list->len; i++) { - sentry_value_decref(list->items[i]); + list_t *old = thing->payload._ptr; + if (sentry__atomic_fetch(&old->refcount) <= 1) { + return true; } - sentry_free(list->items); - sentry_free(list); - break; + list_t *clone = list_clone(old); + if (!clone) { + return false; + } + list_free(old); + thing->payload._ptr = clone; + return true; } case THING_TYPE_OBJECT: { - obj_t *obj = thing->payload._ptr; - for (size_t i = 0; i < obj->len; i++) { - sentry_free(obj->pairs[i].k); - sentry_value_decref(obj->pairs[i].v); + obj_t *old = thing->payload._ptr; + if (sentry__atomic_fetch(&old->refcount) <= 1) { + return true; } - sentry_free(obj->pairs); - sentry_free(obj); + obj_t *clone = obj_clone(old); + if (!clone) { + return false; + } + obj_free(old); + thing->payload._ptr = clone; + return true; + } + default: + return true; + } +} + +static sentry_value_t +thing_get_child(const thing_t *thing, size_t i) +{ + switch (thing_get_type(thing)) { + case THING_TYPE_LIST: + return ((const list_t *)thing->payload._ptr)->items[i]; + case THING_TYPE_OBJECT: + return ((const obj_t *)thing->payload._ptr)->pairs[i].v; + default: + return sentry_value_new_null(); + } +} + +static void +thing_set_child(thing_t *thing, size_t i, sentry_value_t value) +{ + switch (thing_get_type(thing)) { + case THING_TYPE_LIST: + ((list_t *)thing->payload._ptr)->items[i] = value; + break; + case THING_TYPE_OBJECT: + ((obj_t *)thing->payload._ptr)->pairs[i].v = value; + break; + default: break; } - case THING_TYPE_STRING: { +} + +static void +thing_free(thing_t *thing) +{ + switch (thing_get_type(thing)) { + case THING_TYPE_LIST: + list_free(thing->payload._ptr); + break; + case THING_TYPE_OBJECT: + obj_free(thing->payload._ptr); + break; + case THING_TYPE_STRING: sentry_free(thing->payload._ptr); break; } - } sentry_free(thing); } @@ -395,6 +526,7 @@ sentry_value_new_list(void) { list_t *l = SENTRY_MAKE(list_t); if (l) { + l->refcount = 1; sentry_value_t rv = new_thing_value(l, THING_TYPE_LIST); if (sentry_value_is_null(rv)) { sentry_free(l); @@ -410,6 +542,7 @@ sentry__value_new_list_with_size(size_t size) { list_t *l = SENTRY_MAKE(list_t); if (l) { + l->refcount = 1; l->allocated = size; if (size) { l->items = sentry_malloc(sizeof(sentry_value_t) * size); @@ -434,6 +567,7 @@ sentry_value_new_object(void) { obj_t *o = SENTRY_MAKE(obj_t); if (o) { + o->refcount = 1; sentry_value_t rv = new_thing_value(o, THING_TYPE_OBJECT); if (sentry_value_is_null(rv)) { sentry_free(o); @@ -449,6 +583,7 @@ sentry__value_new_object_with_size(size_t size) { obj_t *o = SENTRY_MAKE(obj_t); if (o) { + o->refcount = 1; o->allocated = size; if (size) { o->pairs = sentry_malloc(sizeof(obj_pair_t) * size); @@ -639,7 +774,8 @@ sentry__value_set_by_key_owned( } sentry_slice_t k_slice = { k, k_len }; thing_t *thing = value_as_unfrozen_thing(value); - if (!thing || thing_get_type(thing) != THING_TYPE_OBJECT) { + if (!thing || thing_get_type(thing) != THING_TYPE_OBJECT + || !thing_detach(thing)) { goto fail; } obj_t *o = thing->payload._ptr; @@ -699,7 +835,8 @@ sentry__value_remove_and_take_key_n( } sentry_slice_t k_slice = { k, k_len }; thing_t *thing = value_as_unfrozen_thing(value); - if (!thing || thing_get_type(thing) != THING_TYPE_OBJECT) { + if (!thing || thing_get_type(thing) != THING_TYPE_OBJECT + || !thing_detach(thing)) { return NULL; } obj_t *o = thing->payload._ptr; @@ -742,7 +879,8 @@ int sentry_value_append(sentry_value_t value, sentry_value_t v) { thing_t *thing = value_as_unfrozen_thing(value); - if (!thing || thing_get_type(thing) != THING_TYPE_LIST) { + if (!thing || thing_get_type(thing) != THING_TYPE_LIST + || !thing_detach(thing)) { goto fail; } @@ -811,6 +949,42 @@ sentry__value_stringify(sentry_value_t value) #undef STRINGIFY_NUMERIC } +static bool +value_is_container(sentry_value_t value) +{ + const thing_t *thing = value_as_thing(value); + if (!thing) { + return false; + } + int type = thing_get_type(thing); + return type == THING_TYPE_LIST || type == THING_TYPE_OBJECT; +} + +static bool +thing_clone_children(thing_t *thing, size_t len) +{ + bool detached = false; + for (size_t i = 0; i < len; i++) { + sentry_value_t child = thing_get_child(thing, i); + if (!value_is_container(child)) { + continue; + } + if (!detached) { + if (!thing_detach(thing)) { + return false; + } + detached = true; + } + sentry_value_t cloned = sentry__value_clone(child); + if (sentry_value_is_null(cloned)) { + return false; + } + sentry_value_decref(child); + thing_set_child(thing, i, cloned); + } + return true; +} + sentry_value_t sentry__value_clone(sentry_value_t value) { @@ -820,20 +994,30 @@ sentry__value_clone(sentry_value_t value) } switch (thing_get_type(thing)) { case THING_TYPE_LIST: { - const list_t *list = thing->payload._ptr; - sentry_value_t rv = sentry__value_new_list_with_size(list->len); - for (size_t i = 0; i < list->len; i++) { - sentry_value_incref(list->items[i]); - sentry_value_append(rv, list->items[i]); + list_t *list = thing->payload._ptr; + sentry__atomic_fetch_and_add(&list->refcount, 1); + sentry_value_t rv = new_thing_value(list, THING_TYPE_LIST); + if (sentry_value_is_null(rv)) { + list_free(list); + return rv; + } + if (!thing_clone_children(value_as_thing(rv), list->len)) { + sentry_value_decref(rv); + return sentry_value_new_null(); } return rv; } case THING_TYPE_OBJECT: { - const obj_t *obj = thing->payload._ptr; - sentry_value_t rv = sentry__value_new_object_with_size(obj->len); - for (size_t i = 0; i < obj->len; i++) { - sentry_value_incref(obj->pairs[i].v); - sentry_value_set_by_key(rv, obj->pairs[i].k, obj->pairs[i].v); + obj_t *obj = thing->payload._ptr; + sentry__atomic_fetch_and_add(&obj->refcount, 1); + sentry_value_t rv = new_thing_value(obj, THING_TYPE_OBJECT); + if (sentry_value_is_null(rv)) { + obj_free(obj); + return rv; + } + if (!thing_clone_children(value_as_thing(rv), obj->len)) { + sentry_value_decref(rv); + return sentry_value_new_null(); } return rv; } @@ -852,7 +1036,8 @@ int sentry_value_set_by_index(sentry_value_t value, size_t index, sentry_value_t v) { thing_t *thing = value_as_unfrozen_thing(value); - if (!thing || thing_get_type(thing) != THING_TYPE_LIST) { + if (!thing || thing_get_type(thing) != THING_TYPE_LIST + || !thing_detach(thing)) { goto fail; } @@ -882,7 +1067,8 @@ int sentry_value_remove_by_index(sentry_value_t value, size_t index) { thing_t *thing = value_as_unfrozen_thing(value); - if (!thing || thing_get_type(thing) != THING_TYPE_LIST) { + if (!thing || thing_get_type(thing) != THING_TYPE_LIST + || !thing_detach(thing)) { return 1; } diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 9612c2f60..eed1e9eaa 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -983,6 +983,88 @@ SENTRY_TEST(before_breadcrumb_passthrough) sentry_close(); } +static sentry_value_t +before_send_modify_scope_values( + sentry_value_t event, void *UNUSED(hint), void *UNUSED(data)) +{ + sentry_value_t contexts = sentry_value_get_by_key(event, "contexts"); + sentry_value_t gpu = sentry_value_get_by_key(contexts, "gpu"); + sentry_value_set_by_key(gpu, "name", sentry_value_new_string("modified")); + sentry_value_set_by_key( + gpu, "injected", sentry_value_new_string("injected")); + + sentry_value_t extra = sentry_value_get_by_key(event, "extra"); + sentry_value_t data_obj = sentry_value_get_by_key(extra, "data"); + sentry_value_set_by_key( + data_obj, "key", sentry_value_new_string("modified")); + sentry_value_set_by_key( + data_obj, "injected", sentry_value_new_string("injected")); + + sentry_value_t user = sentry_value_get_by_key(event, "user"); + sentry_value_set_by_key( + user, "username", sentry_value_new_string("modified")); + sentry_value_set_by_key( + user, "injected", sentry_value_new_string("injected")); + + sentry_value_t fingerprint = sentry_value_get_by_key(event, "fingerprint"); + sentry_value_append(fingerprint, sentry_value_new_string("injected")); + + return event; +} + +SENTRY_TEST(scope_clone) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_before_send( + options, before_send_modify_scope_values, NULL); + sentry_init(options); + + sentry_value_t gpu = sentry_value_new_object(); + sentry_value_set_by_key(gpu, "name", sentry_value_new_string("original")); + sentry_set_context("gpu", gpu); + + sentry_value_t data = sentry_value_new_object(); + sentry_value_set_by_key(data, "key", sentry_value_new_string("original")); + sentry_set_extra("data", data); + + sentry_set_user(sentry_value_new_user("1", "original", NULL, NULL)); + sentry_set_fingerprint("fp1", "fp2", NULL); + + // before_send modifies contexts, extra, user, and fingerprint on the event + sentry_capture_event( + sentry_value_new_message_event(SENTRY_LEVEL_INFO, NULL, "test")); + + // scope values must not be corrupted by before_send modifications + SENTRY_WITH_SCOPE (scope) { + sentry_value_t scope_gpu + = sentry_value_get_by_key(scope->contexts, "gpu"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(scope_gpu, "name")), + "original"); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(scope_gpu, "injected"))); + + sentry_value_t scope_data + = sentry_value_get_by_key(scope->extra, "data"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(scope_data, "key")), + "original"); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(scope_data, "injected"))); + + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + scope->user, "username")), + "original"); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(scope->user, "injected"))); + + TEST_CHECK_INT_EQUAL(sentry_value_get_length(scope->fingerprint), 2); + } + + sentry_close(); +} + SENTRY_TEST(scope_global_attributes) { SENTRY_TEST_OPTIONS_NEW(options); diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index 99c72db99..41c660f1f 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -442,6 +442,238 @@ SENTRY_TEST(value_object_merge_nested) sentry_value_decref(dst); } +SENTRY_TEST(value_clone_list) +{ + sentry_value_t original = sentry_value_new_list(); + sentry_value_append(original, sentry_value_new_int32(1)); + sentry_value_append(original, sentry_value_new_int32(2)); + sentry_value_append(original, sentry_value_new_int32(3)); + + sentry_value_t clone = sentry__value_clone(original); + TEST_CHECK(sentry_value_refcount(clone) == 1); + TEST_CHECK_JSON_VALUE(clone, "[1,2,3]"); + + // mutating the clone triggers COW — original is unaffected + sentry_value_append(clone, sentry_value_new_int32(4)); + TEST_CHECK_JSON_VALUE(clone, "[1,2,3,4]"); + TEST_CHECK_JSON_VALUE(original, "[1,2,3]"); + + sentry_value_set_by_index(clone, 0, sentry_value_new_int32(10)); + TEST_CHECK_JSON_VALUE(clone, "[10,2,3,4]"); + TEST_CHECK_JSON_VALUE(original, "[1,2,3]"); + + sentry_value_remove_by_index(clone, 1); + TEST_CHECK_JSON_VALUE(clone, "[10,3,4]"); + TEST_CHECK_JSON_VALUE(original, "[1,2,3]"); + + sentry_value_decref(clone); + // original still valid after clone is freed + TEST_CHECK_JSON_VALUE(original, "[1,2,3]"); + sentry_value_decref(original); +} + +SENTRY_TEST(value_clone_object) +{ + sentry_value_t original = sentry_value_new_object(); + sentry_value_set_by_key(original, "a", sentry_value_new_int32(1)); + sentry_value_set_by_key(original, "b", sentry_value_new_int32(2)); + + sentry_value_t clone = sentry__value_clone(original); + TEST_CHECK(sentry_value_refcount(clone) == 1); + TEST_CHECK_JSON_VALUE(clone, "{\"a\":1,\"b\":2}"); + + // mutating the clone triggers COW — original is unaffected + sentry_value_set_by_key(clone, "a", sentry_value_new_int32(10)); + TEST_CHECK_INT_EQUAL( + sentry_value_as_int32(sentry_value_get_by_key(clone, "a")), 10); + TEST_CHECK_INT_EQUAL( + sentry_value_as_int32(sentry_value_get_by_key(original, "a")), 1); + + sentry_value_set_by_key(clone, "c", sentry_value_new_int32(3)); + TEST_CHECK(sentry_value_get_length(clone) == 3); + TEST_CHECK(sentry_value_get_length(original) == 2); + + sentry_value_remove_by_key(clone, "b"); + TEST_CHECK(sentry_value_get_length(clone) == 2); + TEST_CHECK(sentry_value_get_length(original) == 2); + + sentry_value_decref(clone); + TEST_CHECK_JSON_VALUE(original, "{\"a\":1,\"b\":2}"); + sentry_value_decref(original); +} + +SENTRY_TEST(value_clone_frozen) +{ + sentry_value_t original = sentry_value_new_object(); + sentry_value_set_by_key(original, "a", sentry_value_new_int32(1)); + sentry_value_freeze(original); + TEST_CHECK(sentry_value_is_frozen(original)); + + sentry_value_t clone = sentry__value_clone(original); + TEST_CHECK(!sentry_value_is_frozen(clone)); + + // clone is mutable even though original is frozen + sentry_value_set_by_key(clone, "b", sentry_value_new_int32(2)); + TEST_CHECK_JSON_VALUE(clone, "{\"a\":1,\"b\":2}"); + TEST_CHECK_JSON_VALUE(original, "{\"a\":1}"); + + sentry_value_decref(clone); + sentry_value_decref(original); +} + +SENTRY_TEST(value_clone_multiple) +{ + sentry_value_t original = sentry_value_new_list(); + sentry_value_append(original, sentry_value_new_int32(1)); + + sentry_value_t clone1 = sentry__value_clone(original); + sentry_value_t clone2 = sentry__value_clone(original); + + // each clone can be mutated independently + sentry_value_append(clone1, sentry_value_new_int32(2)); + sentry_value_append(clone2, sentry_value_new_int32(3)); + + TEST_CHECK_JSON_VALUE(original, "[1]"); + TEST_CHECK_JSON_VALUE(clone1, "[1,2]"); + TEST_CHECK_JSON_VALUE(clone2, "[1,3]"); + + sentry_value_decref(original); + sentry_value_decref(clone1); + sentry_value_decref(clone2); +} + +SENTRY_TEST(value_clone_of_clone) +{ + sentry_value_t original = sentry_value_new_object(); + sentry_value_set_by_key(original, "x", sentry_value_new_int32(1)); + + sentry_value_t clone1 = sentry__value_clone(original); + sentry_value_t clone2 = sentry__value_clone(clone1); + + sentry_value_set_by_key(clone2, "y", sentry_value_new_int32(2)); + TEST_CHECK_JSON_VALUE(original, "{\"x\":1}"); + TEST_CHECK_JSON_VALUE(clone1, "{\"x\":1}"); + TEST_CHECK_JSON_VALUE(clone2, "{\"x\":1,\"y\":2}"); + + sentry_value_decref(original); + sentry_value_decref(clone1); + sentry_value_decref(clone2); +} + +SENTRY_TEST(value_clone_merge) +{ + sentry_value_t original = sentry_value_new_object(); + sentry_value_set_by_key(original, "a", sentry_value_new_int32(1)); + + sentry_value_t clone = sentry__value_clone(original); + + sentry_value_t src = sentry_value_new_object(); + sentry_value_set_by_key(src, "b", sentry_value_new_int32(2)); + + int rv = sentry__value_merge_objects(clone, src); + TEST_CHECK_INT_EQUAL(rv, 0); + sentry_value_decref(src); + + TEST_CHECK_JSON_VALUE(clone, "{\"a\":1,\"b\":2}"); + TEST_CHECK_JSON_VALUE(original, "{\"a\":1}"); + + sentry_value_decref(clone); + sentry_value_decref(original); +} + +SENTRY_TEST(value_clone_free_original_first) +{ + sentry_value_t original = sentry_value_new_list(); + sentry_value_append(original, sentry_value_new_string("hello")); + sentry_value_append(original, sentry_value_new_int32(42)); + + sentry_value_t clone = sentry__value_clone(original); + // free original while clone still shares data + sentry_value_decref(original); + + TEST_CHECK_JSON_VALUE(clone, "[\"hello\",42]"); + sentry_value_append(clone, sentry_value_new_int32(3)); + TEST_CHECK_JSON_VALUE(clone, "[\"hello\",42,3]"); + sentry_value_decref(clone); +} + +SENTRY_TEST(value_clone_nested_modify_leaf) +{ + // {ctx: {name: "os", ver: "14"}, tag: "v1"} + sentry_value_t original = sentry_value_new_object(); + sentry_value_t child = sentry_value_new_object(); + sentry_value_set_by_key(child, "name", sentry_value_new_string("os")); + sentry_value_set_by_key(child, "ver", sentry_value_new_string("14")); + sentry_value_set_by_key(original, "ctx", child); + sentry_value_set_by_key(original, "tag", sentry_value_new_string("v1")); + + sentry_value_t clone = sentry__value_clone(original); + + // mutate a leaf key on the clone — original unaffected + sentry_value_set_by_key(clone, "tag", sentry_value_new_string("v2")); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(original, "tag")), "v1"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(clone, "tag")), "v2"); + + // nested child objects are independent + sentry_value_t clone_ctx = sentry_value_get_by_key(clone, "ctx"); + sentry_value_set_by_key(clone_ctx, "name", sentry_value_new_string("win")); + sentry_value_set_by_key(clone_ctx, "arch", sentry_value_new_string("x64")); + + sentry_value_t orig_ctx = sentry_value_get_by_key(original, "ctx"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(orig_ctx, "name")), + "os"); + TEST_CHECK(sentry_value_is_null(sentry_value_get_by_key(orig_ctx, "arch"))); + TEST_CHECK(sentry_value_get_length(orig_ctx) == 2); + + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(clone_ctx, "name")), + "win"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(clone_ctx, "arch")), + "x64"); + TEST_CHECK(sentry_value_get_length(clone_ctx) == 3); + + sentry_value_decref(clone); + sentry_value_decref(original); +} + +SENTRY_TEST(value_clone_nested_modify_middle) +{ + // [[1, 2], [3, 4]] + sentry_value_t original = sentry_value_new_list(); + sentry_value_t inner0 = sentry_value_new_list(); + sentry_value_append(inner0, sentry_value_new_int32(1)); + sentry_value_append(inner0, sentry_value_new_int32(2)); + sentry_value_t inner1 = sentry_value_new_list(); + sentry_value_append(inner1, sentry_value_new_int32(3)); + sentry_value_append(inner1, sentry_value_new_int32(4)); + sentry_value_append(original, inner0); + sentry_value_append(original, inner1); + + sentry_value_t clone = sentry__value_clone(original); + + // replace a whole nested list in the clone (middle-level mutation) + sentry_value_t replacement = sentry_value_new_list(); + sentry_value_append(replacement, sentry_value_new_int32(99)); + sentry_value_set_by_index(clone, 0, replacement); + + TEST_CHECK_JSON_VALUE(original, "[[1,2],[3,4]]"); + TEST_CHECK_JSON_VALUE(clone, "[[99],[3,4]]"); + + // mutate a leaf inside the other nested list + sentry_value_t clone_inner1 = sentry_value_get_by_index(clone, 1); + sentry_value_append(clone_inner1, sentry_value_new_int32(5)); + + TEST_CHECK_JSON_VALUE(original, "[[1,2],[3,4]]"); + TEST_CHECK_JSON_VALUE(clone, "[[99],[3,4,5]]"); + + sentry_value_decref(clone); + sentry_value_decref(original); +} + SENTRY_TEST(value_object_merge_shallow) { sentry_value_t dst = sentry_value_new_object(); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 14afd84a4..06a848795 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -298,6 +298,7 @@ XX(scope_capture_metric_one_shot) XX(scope_capture_metric_user_owned) XX(scope_capture_user_owned) XX(scope_clear) +XX(scope_clone) XX(scope_clone_independence) XX(scope_clone_preserves_data) XX(scope_clone_shares_span) @@ -405,6 +406,15 @@ XX(uuid_api) XX(uuid_v4) XX(value_attribute) XX(value_bool) +XX(value_clone_free_original_first) +XX(value_clone_frozen) +XX(value_clone_list) +XX(value_clone_merge) +XX(value_clone_multiple) +XX(value_clone_nested_modify_leaf) +XX(value_clone_nested_modify_middle) +XX(value_clone_object) +XX(value_clone_of_clone) XX(value_double) XX(value_foreach_key_value) XX(value_freezing)