This is an automated email from the ASF dual-hosted git repository.

nickva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb-jiffy.git

commit c93691c63c72d75720a19c263a250c34d724d0c5
Author: Nick Vatamaniuc <[email protected]>
AuthorDate: Thu Apr 16 15:53:43 2026 -0400

    Improve coverage even more
    
    We slipped in some cases, for example when we bumped the reduction count. 
When
    adding more tests for yielding found out the `byte_per_iter` could become <
    1 (made the test stuck) so add a fix to clip it to 1.
    
    Then there a few more odds and ends for string escaping and options.
---
 c_src/util.c                        | 10 +++++-
 test/jiffy_04_string_tests.erl      | 23 +++++++++++--
 test/jiffy_12_error_tests.erl       | 26 ++++++++++----
 test/jiffy_16_dedupe_keys_tests.erl | 23 +++++++++++++
 test/jiffy_19_yielding_tests.erl    | 68 ++++++++++++++++++++++++++++++++++---
 test/jiffy_util.hrl                 | 13 ++++---
 6 files changed, 146 insertions(+), 17 deletions(-)

diff --git a/c_src/util.c b/c_src/util.c
index cc23a15..a84eabd 100644
--- a/c_src/util.c
+++ b/c_src/util.c
@@ -38,8 +38,12 @@ get_bytes_per_iter(ErlNifEnv* env, ERL_NIF_TERM val, size_t* 
bpi)
         return 0;
     }
 
-    // Calculate the number of bytes per reduction
+    // Calculate the number of bytes per reduction. Clamp to 1 so we
+    // avoid a divide-by-zero in bump_used_reds.
     *bpi = (size_t) (bytes / DEFAULT_ERLANG_REDUCTION_COUNT);
+    if(*bpi == 0) {
+        *bpi = 1;
+    }
 
     return 1;
 }
@@ -68,7 +72,11 @@ get_bytes_per_red(ErlNifEnv* env, ERL_NIF_TERM val, size_t* 
bpi)
         return 0;
     }
 
+    // Same get_bytes_per_iter, clamp to 1 to avoid a divide by 0
     *bpi = (size_t) bytes;
+    if(*bpi == 0) {
+        *bpi = 1;
+    }
 
     return 1;
 }
diff --git a/test/jiffy_04_string_tests.erl b/test/jiffy_04_string_tests.erl
index a8c9446..c96e014 100644
--- a/test/jiffy_04_string_tests.erl
+++ b/test/jiffy_04_string_tests.erl
@@ -146,7 +146,13 @@ cases(error) ->
         % Truncated \uXX (not enough hex digits)
         <<"\"\\u00\"">>,
         % Invalid hex digit in \u escape
-        <<"\"\\uZZZZ\"">>
+        <<"\"\\uZZZZ\"">>,
+        % Same story as \uD834\n but with more trailers to pass the length
+        % guard and reach the '\u' check for the low surrogate. We're down in
+        % the weeds, as it were.
+        <<"\"\\uD834\\nabcdef\"">>,
+        % \uD834\u<bad hex> low surrogate hex error
+        <<"\"\\uD834\\uZZZZ\"">>
     ];
 
 cases(utf8) ->
@@ -205,5 +211,18 @@ cases(bad_utf8_key) ->
 
 cases(escaped_slashes) ->
     [
-        {<<"\"\\/\"">>, <<"/">>}
+        {<<"\"\\/\"">>, <<"/">>},
+        {<<"\"foo\\/bar\\/baz\"">>, <<"foo/bar/baz">>}
+    ].
+
+
+atom_escaped_slashes_test_() ->
+    [
+        ?_assertEqual(<<"\"a\\/b\"">>,
+            enc('a/b', [escape_forward_slashes])),
+        ?_assertEqual(<<"\"a/b\"">>, enc('a/b')),
+        ?_assertEqual(<<"{\"a\\/b\":1}">>,
+            enc({[{'a/b', 1}]}, [escape_forward_slashes])),
+        ?_assertEqual(<<"\"foo\\/bar\\/baz\\/potato\"">>,
+            enc('foo/bar/baz/potato', [escape_forward_slashes]))
     ].
diff --git a/test/jiffy_12_error_tests.erl b/test/jiffy_12_error_tests.erl
index fa636f2..7a1ed9f 100644
--- a/test/jiffy_12_error_tests.erl
+++ b/test/jiffy_12_error_tests.erl
@@ -82,12 +82,26 @@ enc_invalid_object_member_key_test_() ->
     ]}.
 
 
-encode_bad_option_test() ->
-    ?assertError(badarg, jiffy:encode(1, [not_a_valid_option])).
-
-
-decode_bad_option_test() ->
-    ?assertError(badarg, jiffy:decode(<<"1">>, [not_a_valid_option])).
+decode_bad_option_test_() ->
+    [?_assertError(badarg, jiffy:decode(<<"1">>, [O])) || O <- [
+        not_a_valid_option,
+        <<"foo">>,
+        {foo, bar, baz},
+        {bytes_per_iter, not_an_int},
+        {bytes_per_red, not_an_int},
+        {some_other_opt, value},
+        {null_term, 123}
+    ]].
+
+
+encode_bad_option_test_() ->
+    [?_assertError(badarg, jiffy:encode(1, [O])) || O <- [
+        not_a_valid_option,
+        <<"foo">>,
+        {foo, bar, baz},
+        {bytes_per_iter, not_an_int},
+        {bytes_per_red, not_an_int}
+    ]].
 
 
 enc_error(Type, Obj, Case) ->
diff --git a/test/jiffy_16_dedupe_keys_tests.erl 
b/test/jiffy_16_dedupe_keys_tests.erl
index 7713e51..d78d3a6 100644
--- a/test/jiffy_16_dedupe_keys_tests.erl
+++ b/test/jiffy_16_dedupe_keys_tests.erl
@@ -4,6 +4,7 @@
 -module(jiffy_16_dedupe_keys_tests).
 
 -include_lib("eunit/include/eunit.hrl").
+-include("jiffy_util.hrl").
 
 % Duplicate keys with `return_maps`. We settled on
 % last value wins semantics in such cases so test that
@@ -98,3 +99,25 @@ dedupe_keys_test_() ->
 
 dedupe_keys_empty_test() ->
     ?assertEqual({[]}, jiffy:decode(<<"{}">>, [dedupe_keys])).
+
+% Exercise the heap-allocated dedupe hash table path when count > 64 and hit
+% more than HT_STACK_SLOTS. We're padding those coverage stats here, really.
+dedupe_keys_large_test_() ->
+    N = 100,
+    KV = fun(I) -> [<<"\"">>, i2b(I), <<"\":">>, i2b(I)] end,
+    Body = iol2b(lists:join(",", [KV(I) || I <- lists:seq(1, N)])),
+    Dupes = iol2b(lists:join(",", [KV(I) || I <- lists:seq(1, N)])),
+    JUnique = <<"{", Body/binary, "}">>,
+    JDupes = <<"{", Body/binary, ",", Dupes/binary, "}">>,
+    [
+        {"Unique large object",
+            fun() ->
+                {Pairs} = jiffy:decode(JUnique, [dedupe_keys]),
+                ?assertEqual(N, length(Pairs))
+            end},
+        {"All keys duplicated once",
+            fun() ->
+                {Pairs} = jiffy:decode(JDupes, [dedupe_keys]),
+                ?assertEqual(N, length(Pairs))
+            end}
+    ].
diff --git a/test/jiffy_19_yielding_tests.erl b/test/jiffy_19_yielding_tests.erl
index d12ee88..c64bc72 100644
--- a/test/jiffy_19_yielding_tests.erl
+++ b/test/jiffy_19_yielding_tests.erl
@@ -31,15 +31,75 @@ encode_both_bytes_opts_test() ->
 % otheriwse.
 %
 decode_large_with_bytes_per_red_test() ->
-    Data = iolist_to_binary([
+    Data = iol2b([
         <<"[">>,
         lists:join(<<",">>, [<<"1">> || _ <- lists:seq(1, 500)]),
         <<"]">>
     ]),
-    Result = jiffy:decode(Data, [{bytes_per_red, 20}]),
+    Result = dec(Data, [{bytes_per_red, 20}]),
     ?assertEqual(500, length(Result)).
 
 encode_large_with_bytes_per_red_test() ->
     Data = lists:duplicate(500, 1),
-    Encoded = iolist_to_binary(jiffy:encode(Data, [{bytes_per_red, 20}])),
-    ?assertEqual(Data, jiffy:decode(Encoded)).
+    Encoded = enc(Data, [{bytes_per_red, 20}]),
+    ?assertEqual(Data, dec(Encoded)).
+
+% Nested object but a large binary at the bottom. We want the yield to fire
+% while the term stack holds a large (> SMALL_TERMSTACK_SIZE) entries. Then
+% small byte_per_red to force yielding.
+encode_deep_nesting_yield_test() ->
+    Depth = 12,
+    Big = binary:copy(<<"a">>, 10000),
+    Seq = lists:seq(1, Depth),
+    Nested = lists:foldl(fun(_, Acc) -> {[{<<"k">>, Acc}]} end, Big, Seq),
+    Encoded = enc(Nested, [{bytes_per_red, 1}]),
+    ?assertEqual(Nested, dec(Encoded)).
+
+% Force yielding and test restore/save and schedule bits.
+decode_excessive_yield_test_() ->
+    Data = iol2b([
+        <<"[">>,
+        lists:join(<<",">>, [i2b(I) || I <- lists:seq(1, 2000)]),
+        <<"]">>
+    ]),
+    Expected = lists:seq(1, 2000),
+    [
+        {"bytes_per_red = 1",
+            ?_assertEqual(Expected, dec(Data, [{bytes_per_red, 1}]))},
+        {"bytes_per_red = 0 (clamped to 1)",
+            ?_assertEqual(Expected, dec(Data, [{bytes_per_red, 0}]))},
+        {"bytes_per_iter = 1",
+            ?_assertEqual(Expected, dec(Data, [{bytes_per_iter, 1}]))}
+    ].
+
+encode_excessive_yield_test_() ->
+    Data = lists:seq(1, 2000),
+    [
+        {"bytes_per_red = 1",
+            fun() ->
+                Enc = enc(Data, [{bytes_per_red, 1}]),
+                ?assertEqual(Data, dec(Enc))
+            end},
+        {"bytes_per_red = 0 (clamped to 1)",
+            fun() ->
+                Enc = enc(Data, [{bytes_per_red, 0}]),
+                ?assertEqual(Data, dec(Enc))
+            end},
+        {"bytes_per_iter = 1",
+            fun() ->
+                Enc = enc(Data, [{bytes_per_iter, 1}]),
+                ?assertEqual(Data, dec(Enc))
+            end}
+    ].
+
+% Single large string encoded/decoded + low yield threshold, so
+% we hopefully hit the pct_used > 100 in bump_used_reds
+large_string_yield_test_() ->
+    Big = binary:copy(<<"abc">>, 50000),
+    Json = <<"\"", Big/binary, "\"">>,
+    [
+        {"Decode",
+            ?_assertEqual(Big, dec(Json, [{bytes_per_red, 1}]))},
+        {"Encode",
+            ?_assertEqual(Json, enc(Big, [{bytes_per_red, 1}]))}
+    ].
diff --git a/test/jiffy_util.hrl b/test/jiffy_util.hrl
index 983f7e3..9fd7d6b 100644
--- a/test/jiffy_util.hrl
+++ b/test/jiffy_util.hrl
@@ -4,12 +4,18 @@
 -compile(export_all).
 -compile(nowarn_export_all).
 
+iol2b(X) ->
+    iolist_to_binary(X).
+
+i2b(X) ->
+    integer_to_binary(X).
+
 msg(Fmt, Args) ->
     M1 = io_lib:format(Fmt, Args),
     M2 = re:replace(M1, <<"\r">>, <<"\\\\r">>, [global]),
     M3 = re:replace(M2, <<"\n">>, <<"\\\\n">>, [global]),
     M4 = re:replace(M3, <<"\t">>, <<"\\\\t">>, [global]),
-    iolist_to_binary(M4).
+    iol2b(M4).
 
 
 hex(Bin) when is_binary(Bin) ->
@@ -27,12 +33,11 @@ dec(V, Opts) ->
 
 
 enc(V) ->
-    iolist_to_binary(jiffy:encode(V)).
+    iol2b(jiffy:encode(V)).
 
 
 enc(V, Opts) ->
-    iolist_to_binary(jiffy:encode(V, Opts)).
-
+    iol2b(jiffy:encode(V, Opts)).
 
 %% rebar runs eunit with PWD as .eunit/
 %% rebar3 runs eunit with PWD as ./

Reply via email to