From 381d7b0134e778f484e52d2a3ce0951a096563a7 Mon Sep 17 00:00:00 2001 From: Paul W <167196940+top-sigrid@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:00:57 +0100 Subject: [PATCH 1/8] use loop aproach for diff_objects if max_depth is reached without a diff --- lib/difftastic/differ.rb | 34 +++++- test/diff_objects_adaptive_test.rb | 161 +++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 test/diff_objects_adaptive_test.rb diff --git a/lib/difftastic/differ.rb b/lib/difftastic/differ.rb index e1f327d..3327aef 100644 --- a/lib/difftastic/differ.rb +++ b/lib/difftastic/differ.rb @@ -2,8 +2,12 @@ class Difftastic::Differ DEFAULT_TAB_WIDTH = 2 + DEFAULT_MAX_DEPTH = 5 + DEFAULT_MAX_ITEMS = 10 + DEFAULT_MAX_DEPTH_CAP = 50 + DEFAULT_MAX_ITEMS_CAP = 100 - def initialize(background: nil, color: nil, syntax_highlight: nil, context: nil, width: nil, tab_width: nil, parse_error_limit: nil, underline_highlights: true, left_label: nil, right_label: nil, display: "side-by-side-show-both") + def initialize(background: nil, color: nil, syntax_highlight: nil, context: nil, width: nil, tab_width: nil, parse_error_limit: nil, underline_highlights: true, left_label: nil, right_label: nil, display: "side-by-side-show-both", max_depth: nil, max_items: nil, max_depth_cap: nil, max_items_cap: nil) @show_paths = false @background = background => :dark | :light | nil @color = color => :always | :never | :auto | nil @@ -16,15 +20,37 @@ def initialize(background: nil, color: nil, syntax_highlight: nil, context: nil, @left_label = left_label => String | nil @right_label = right_label => String | nil @display = display + @max_depth = max_depth => Integer | nil + @max_items = max_items => Integer | nil + @max_depth_cap = max_depth_cap => Integer | nil + @max_items_cap = max_items_cap => Integer | nil end def diff_objects(old, new) tab_width = @tab_width || DEFAULT_TAB_WIDTH + max_depth = @max_depth || DEFAULT_MAX_DEPTH + max_items = @max_items || DEFAULT_MAX_ITEMS + max_depth_cap = @max_depth_cap || DEFAULT_MAX_DEPTH_CAP + max_items_cap = @max_items_cap || DEFAULT_MAX_ITEMS_CAP + + loop do + old_str = Difftastic.pretty(old, tab_width:, max_depth:, max_items:) + new_str = Difftastic.pretty(new, tab_width:, max_depth:, max_items:) + + # If prettified strings differ, we have enough depth/items to show the difference + if old_str != new_str + return diff_strings(old_str, new_str, file_extension: "rb") + end - old = Difftastic.pretty(old, tab_width:) - new = Difftastic.pretty(new, tab_width:) + # If we've hit both caps, stop trying + if max_depth >= max_depth_cap && max_items >= max_items_cap + return diff_strings(old_str, new_str, file_extension: "rb") + end - diff_strings(old, new, file_extension: "rb") + # Increase limits and retry + max_depth = [max_depth + 5, max_depth_cap].min + max_items = [max_items + 10, max_items_cap].min + end end def diff_ada(old, new) diff --git a/test/diff_objects_adaptive_test.rb b/test/diff_objects_adaptive_test.rb new file mode 100644 index 0000000..8318104 --- /dev/null +++ b/test/diff_objects_adaptive_test.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class DiffObjectsAdaptiveTest < Minitest::Spec + DEFAULT_MAX_DEPTH = Difftastic::Differ::DEFAULT_MAX_DEPTH + DEFAULT_MAX_ITEMS = Difftastic::Differ::DEFAULT_MAX_ITEMS + + def nested_hash(depth, leaf_value) + (1..depth).reverse_each.reduce(leaf_value) { |inner, i| { "l#{i}": inner } } + end + + def array_with_diff_at(position, value) + (1...position).to_a + [value] + end + + describe "adaptive max_depth" do + it "shows diff at exactly DEFAULT_MAX_DEPTH" do + old = nested_hash(DEFAULT_MAX_DEPTH, "old") + new = nested_hash(DEFAULT_MAX_DEPTH, "new") + + output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + + refute_includes output, "No changes" + assert_includes output, "old" + assert_includes output, "new" + end + + it "shows diff at DEFAULT_MAX_DEPTH + 1 (requires adaptation)" do + depth = DEFAULT_MAX_DEPTH + 1 + old = nested_hash(depth, "old") + new = nested_hash(depth, "new") + + output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + + refute_includes output, "No changes" + assert_includes output, "old" + assert_includes output, "new" + end + + it "shows diff at 2 * DEFAULT_MAX_DEPTH (requires multiple adaptations)" do + depth = 2 * DEFAULT_MAX_DEPTH + old = nested_hash(depth, "old") + new = nested_hash(depth, "new") + + output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + + refute_includes output, "No changes" + assert_includes output, "old" + assert_includes output, "new" + end + end + + describe "adaptive max_items" do + it "shows diff at exactly DEFAULT_MAX_ITEMS" do + old = array_with_diff_at(DEFAULT_MAX_ITEMS, "old") + new = array_with_diff_at(DEFAULT_MAX_ITEMS, "new") + + output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + + refute_includes output, "No changes" + assert_includes output, "old" + assert_includes output, "new" + end + + it "shows diff at DEFAULT_MAX_ITEMS + 1 (requires adaptation)" do + position = DEFAULT_MAX_ITEMS + 1 + old = array_with_diff_at(position, "old") + new = array_with_diff_at(position, "new") + + output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + + refute_includes output, "No changes" + assert_includes output, "old" + assert_includes output, "new" + end + + it "shows diff at 2 * DEFAULT_MAX_ITEMS (requires multiple adaptations)" do + position = 2 * DEFAULT_MAX_ITEMS + old = array_with_diff_at(position, "old") + new = array_with_diff_at(position, "new") + + output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + + refute_includes output, "No changes" + assert_includes output, "old" + assert_includes output, "new" + end + end + + describe "configurable starting values" do + it "respects custom max_depth" do + depth = DEFAULT_MAX_DEPTH - 2 + old = nested_hash(depth, "old") + new = nested_hash(depth, "new") + + output = Difftastic::Differ.new(color: :never, max_depth: 1).diff_objects(old, new) + + refute_includes output, "No changes" + end + + it "respects custom max_items" do + position = DEFAULT_MAX_ITEMS - 5 + old = array_with_diff_at(position, "old") + new = array_with_diff_at(position, "new") + + output = Difftastic::Differ.new(color: :never, max_items: 2).diff_objects(old, new) + + refute_includes output, "No changes" + end + end + + describe "configurable caps" do + it "respects custom max_depth_cap" do + depth = DEFAULT_MAX_DEPTH + 3 + old = nested_hash(depth, "old") + new = nested_hash(depth, "new") + + output = Difftastic::Differ.new(color: :never, max_depth_cap: depth + 1).diff_objects(old, new) + + refute_includes output, "No changes" + end + + it "respects custom max_items_cap" do + position = DEFAULT_MAX_ITEMS + 5 + old = array_with_diff_at(position, "old") + new = array_with_diff_at(position, "new") + + output = Difftastic::Differ.new(color: :never, max_items_cap: position + 1).diff_objects(old, new) + + refute_includes output, "No changes" + end + end + + describe "real-world structures" do + it "handles typical API request body" do + old = { + type: "Request", + positions: [{ + address: { + sender: { postalCode: "41564", city: "Kaarst" } + } + }] + } + new = { + type: "Request", + positions: [{ + address: { + sender: { postalCode: "99999", city: "Berlin" } + } + }] + } + + output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + + refute_includes output, "No changes" + assert_includes output, "41564" + assert_includes output, "99999" + end + end +end From 299d87a0ca9642acab0c5a823f6d2e3690661f5e Mon Sep 17 00:00:00 2001 From: Paul W <167196940+top-sigrid@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:48:35 +0100 Subject: [PATCH 2/8] Better tests; Use constant for increase step; More comments --- lib/difftastic/differ.rb | 18 ++-- test/diff_objects_adaptive_test.rb | 128 +++++++++++++++++------------ 2 files changed, 87 insertions(+), 59 deletions(-) diff --git a/lib/difftastic/differ.rb b/lib/difftastic/differ.rb index 3327aef..3751a9c 100644 --- a/lib/difftastic/differ.rb +++ b/lib/difftastic/differ.rb @@ -4,8 +4,10 @@ class Difftastic::Differ DEFAULT_TAB_WIDTH = 2 DEFAULT_MAX_DEPTH = 5 DEFAULT_MAX_ITEMS = 10 - DEFAULT_MAX_DEPTH_CAP = 50 - DEFAULT_MAX_ITEMS_CAP = 100 + DEFAULT_MAX_DEPTH_CAP = 20 + DEFAULT_MAX_ITEMS_CAP = 40 + MAX_DEPTH_INCREMENT = 5 + MAX_ITEMS_INCREMENT = 10 def initialize(background: nil, color: nil, syntax_highlight: nil, context: nil, width: nil, tab_width: nil, parse_error_limit: nil, underline_highlights: true, left_label: nil, right_label: nil, display: "side-by-side-show-both", max_depth: nil, max_items: nil, max_depth_cap: nil, max_items_cap: nil) @show_paths = false @@ -37,7 +39,10 @@ def diff_objects(old, new) old_str = Difftastic.pretty(old, tab_width:, max_depth:, max_items:) new_str = Difftastic.pretty(new, tab_width:, max_depth:, max_items:) - # If prettified strings differ, we have enough depth/items to show the difference + # If prettified strings don't differ, the strings probably where truncated to max_depth and/or max_items + # PrettyPlease then correctly did not return un-matching strings + # In that case we increase max_depth & max_items in the loop + # until DEFAULT_MAX_ITEMS_CAP or DEFAULT_MAX_DEPTH_CAP is exceeded if old_str != new_str return diff_strings(old_str, new_str, file_extension: "rb") end @@ -47,9 +52,10 @@ def diff_objects(old, new) return diff_strings(old_str, new_str, file_extension: "rb") end - # Increase limits and retry - max_depth = [max_depth + 5, max_depth_cap].min - max_items = [max_items + 10, max_items_cap].min + # Increase limits and retry, while never increasing to more than max_depth_cap/max_items_cap + # Both values are matched to perform 3 retries, but neither value would increase to more than its max anyways + max_depth = [max_depth + MAX_DEPTH_INCREMENT, max_depth_cap].min + max_items = [max_items + MAX_ITEMS_INCREMENT, max_items_cap].min end end diff --git a/test/diff_objects_adaptive_test.rb b/test/diff_objects_adaptive_test.rb index 8318104..d7af1ea 100644 --- a/test/diff_objects_adaptive_test.rb +++ b/test/diff_objects_adaptive_test.rb @@ -3,47 +3,64 @@ require_relative "test_helper" class DiffObjectsAdaptiveTest < Minitest::Spec - DEFAULT_MAX_DEPTH = Difftastic::Differ::DEFAULT_MAX_DEPTH - DEFAULT_MAX_ITEMS = Difftastic::Differ::DEFAULT_MAX_ITEMS + # Use smaller values than the gem's defaults to keep tests fast. + # The adaptive logic works the same regardless of the actual values. + TEST_MAX_DEPTH = 3 + TEST_MAX_ITEMS = 5 + + def differ(**options) + Difftastic::Differ.new( + color: :never, + max_depth: TEST_MAX_DEPTH, + max_items: TEST_MAX_ITEMS, + **options # overrides defaults when provided + ) + end + # Example: + # nested_hash(4, "x") + # # => { l1: { l2: { l3: { l4: "x" } } } } def nested_hash(depth, leaf_value) (1..depth).reverse_each.reduce(leaf_value) { |inner, i| { "l#{i}": inner } } end + # Example: + # array_with_diff_at(4, "x") + # # => [1, 2, 3, "x"] def array_with_diff_at(position, value) (1...position).to_a + [value] end describe "adaptive max_depth" do - it "shows diff at exactly DEFAULT_MAX_DEPTH" do - old = nested_hash(DEFAULT_MAX_DEPTH, "old") - new = nested_hash(DEFAULT_MAX_DEPTH, "new") + it "shows diff at exactly TEST_MAX_DEPTH" do + old = nested_hash(TEST_MAX_DEPTH, "old") + new = nested_hash(TEST_MAX_DEPTH, "new") - output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + output = differ.diff_objects(old, new) refute_includes output, "No changes" assert_includes output, "old" assert_includes output, "new" end - it "shows diff at DEFAULT_MAX_DEPTH + 1 (requires adaptation)" do - depth = DEFAULT_MAX_DEPTH + 1 + it "shows diff at TEST_MAX_DEPTH + 1 (requires adaptation)" do + depth = TEST_MAX_DEPTH + 1 old = nested_hash(depth, "old") new = nested_hash(depth, "new") - output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + output = differ.diff_objects(old, new) refute_includes output, "No changes" assert_includes output, "old" assert_includes output, "new" end - it "shows diff at 2 * DEFAULT_MAX_DEPTH (requires multiple adaptations)" do - depth = 2 * DEFAULT_MAX_DEPTH + it "shows diff at 2 * TEST_MAX_DEPTH (requires multiple adaptations)" do + depth = 2 * TEST_MAX_DEPTH old = nested_hash(depth, "old") new = nested_hash(depth, "new") - output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + output = differ.diff_objects(old, new) refute_includes output, "No changes" assert_includes output, "old" @@ -52,35 +69,35 @@ def array_with_diff_at(position, value) end describe "adaptive max_items" do - it "shows diff at exactly DEFAULT_MAX_ITEMS" do - old = array_with_diff_at(DEFAULT_MAX_ITEMS, "old") - new = array_with_diff_at(DEFAULT_MAX_ITEMS, "new") + it "shows diff at exactly TEST_MAX_ITEMS" do + old = array_with_diff_at(TEST_MAX_ITEMS, "old") + new = array_with_diff_at(TEST_MAX_ITEMS, "new") - output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + output = differ.diff_objects(old, new) refute_includes output, "No changes" assert_includes output, "old" assert_includes output, "new" end - it "shows diff at DEFAULT_MAX_ITEMS + 1 (requires adaptation)" do - position = DEFAULT_MAX_ITEMS + 1 + it "shows diff at TEST_MAX_ITEMS + 1 (requires adaptation)" do + position = TEST_MAX_ITEMS + 1 old = array_with_diff_at(position, "old") new = array_with_diff_at(position, "new") - output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + output = differ.diff_objects(old, new) refute_includes output, "No changes" assert_includes output, "old" assert_includes output, "new" end - it "shows diff at 2 * DEFAULT_MAX_ITEMS (requires multiple adaptations)" do - position = 2 * DEFAULT_MAX_ITEMS + it "shows diff at 2 * TEST_MAX_ITEMS (requires multiple adaptations)" do + position = 2 * TEST_MAX_ITEMS old = array_with_diff_at(position, "old") new = array_with_diff_at(position, "new") - output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + output = differ.diff_objects(old, new) refute_includes output, "No changes" assert_includes output, "old" @@ -90,72 +107,77 @@ def array_with_diff_at(position, value) describe "configurable starting values" do it "respects custom max_depth" do - depth = DEFAULT_MAX_DEPTH - 2 + depth = TEST_MAX_DEPTH - 2 old = nested_hash(depth, "old") new = nested_hash(depth, "new") - output = Difftastic::Differ.new(color: :never, max_depth: 1).diff_objects(old, new) + output = differ(max_depth: 1).diff_objects(old, new) refute_includes output, "No changes" + assert_includes output, "old" + assert_includes output, "new" end it "respects custom max_items" do - position = DEFAULT_MAX_ITEMS - 5 + position = TEST_MAX_ITEMS - 2 old = array_with_diff_at(position, "old") new = array_with_diff_at(position, "new") - output = Difftastic::Differ.new(color: :never, max_items: 2).diff_objects(old, new) + output = differ(max_items: 1).diff_objects(old, new) refute_includes output, "No changes" + assert_includes output, "old" + assert_includes output, "new" end end describe "configurable caps" do it "respects custom max_depth_cap" do - depth = DEFAULT_MAX_DEPTH + 3 + depth = TEST_MAX_DEPTH + 3 old = nested_hash(depth, "old") new = nested_hash(depth, "new") - output = Difftastic::Differ.new(color: :never, max_depth_cap: depth + 1).diff_objects(old, new) + output = differ(max_depth_cap: depth + 1).diff_objects(old, new) refute_includes output, "No changes" + assert_includes output, "old" + assert_includes output, "new" end it "respects custom max_items_cap" do - position = DEFAULT_MAX_ITEMS + 5 + position = TEST_MAX_ITEMS + 5 old = array_with_diff_at(position, "old") new = array_with_diff_at(position, "new") - output = Difftastic::Differ.new(color: :never, max_items_cap: position + 1).diff_objects(old, new) + output = differ(max_items_cap: position + 1).diff_objects(old, new) refute_includes output, "No changes" + assert_includes output, "old" + assert_includes output, "new" end end - describe "real-world structures" do - it "handles typical API request body" do - old = { - type: "Request", - positions: [{ - address: { - sender: { postalCode: "41564", city: "Kaarst" } - } - }] - } - new = { - type: "Request", - positions: [{ - address: { - sender: { postalCode: "99999", city: "Berlin" } - } - }] - } - - output = Difftastic::Differ.new(color: :never).diff_objects(old, new) + describe "loop termination at caps" do + it "terminates with 'No changes' when depth exceeds max_depth_cap" do + cap = TEST_MAX_DEPTH + 2 + depth = cap + 1 + old = nested_hash(depth, "old") + new = nested_hash(depth, "new") - refute_includes output, "No changes" - assert_includes output, "41564" - assert_includes output, "99999" + output = differ(max_depth_cap: cap).diff_objects(old, new) + + assert_includes output, "No changes", "Loop should terminate at cap and return 'No changes'" + end + + it "terminates with 'No changes' when position exceeds max_items_cap" do + cap = TEST_MAX_ITEMS + 5 + position = cap + 1 + old = array_with_diff_at(position, "old") + new = array_with_diff_at(position, "new") + + output = differ(max_items_cap: cap).diff_objects(old, new) + + assert_includes output, "No changes", "Loop should terminate at cap and return 'No changes'" end end end From 8818abd2fb6455ccb62fac4b7021135529a075f5 Mon Sep 17 00:00:00 2001 From: Paul W <167196940+top-sigrid@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:43:25 +0100 Subject: [PATCH 3/8] Change return message --- lib/difftastic/differ.rb | 3 ++- test/diff_objects_adaptive_test.rb | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/difftastic/differ.rb b/lib/difftastic/differ.rb index 3751a9c..4ab8ead 100644 --- a/lib/difftastic/differ.rb +++ b/lib/difftastic/differ.rb @@ -8,6 +8,7 @@ class Difftastic::Differ DEFAULT_MAX_ITEMS_CAP = 40 MAX_DEPTH_INCREMENT = 5 MAX_ITEMS_INCREMENT = 10 + DIFF_UNAVAILABLE_MESSAGE = "[Diff unavailable: exceeded depth/size display limits]" def initialize(background: nil, color: nil, syntax_highlight: nil, context: nil, width: nil, tab_width: nil, parse_error_limit: nil, underline_highlights: true, left_label: nil, right_label: nil, display: "side-by-side-show-both", max_depth: nil, max_items: nil, max_depth_cap: nil, max_items_cap: nil) @show_paths = false @@ -49,7 +50,7 @@ def diff_objects(old, new) # If we've hit both caps, stop trying if max_depth >= max_depth_cap && max_items >= max_items_cap - return diff_strings(old_str, new_str, file_extension: "rb") + return DIFF_UNAVAILABLE_MESSAGE end # Increase limits and retry, while never increasing to more than max_depth_cap/max_items_cap diff --git a/test/diff_objects_adaptive_test.rb b/test/diff_objects_adaptive_test.rb index d7af1ea..893b077 100644 --- a/test/diff_objects_adaptive_test.rb +++ b/test/diff_objects_adaptive_test.rb @@ -158,7 +158,7 @@ def array_with_diff_at(position, value) end describe "loop termination at caps" do - it "terminates with 'No changes' when depth exceeds max_depth_cap" do + it "returns unavailable message when depth exceeds max_depth_cap" do cap = TEST_MAX_DEPTH + 2 depth = cap + 1 old = nested_hash(depth, "old") @@ -166,10 +166,10 @@ def array_with_diff_at(position, value) output = differ(max_depth_cap: cap).diff_objects(old, new) - assert_includes output, "No changes", "Loop should terminate at cap and return 'No changes'" + assert_includes output, Difftastic::Differ::DIFF_UNAVAILABLE_MESSAGE end - it "terminates with 'No changes' when position exceeds max_items_cap" do + it "returns unavailable message when position exceeds max_items_cap" do cap = TEST_MAX_ITEMS + 5 position = cap + 1 old = array_with_diff_at(position, "old") @@ -177,7 +177,7 @@ def array_with_diff_at(position, value) output = differ(max_items_cap: cap).diff_objects(old, new) - assert_includes output, "No changes", "Loop should terminate at cap and return 'No changes'" + assert_includes output, Difftastic::Differ::DIFF_UNAVAILABLE_MESSAGE end end end From bcde82f66cf5bc464514d863ffbec9d3df9aa6a2 Mon Sep 17 00:00:00 2001 From: Paul W <167196940+top-sigrid@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:53:06 +0100 Subject: [PATCH 4/8] User smaller increments in test --- lib/difftastic/differ.rb | 16 ++++++++++------ test/diff_objects_adaptive_test.rb | 3 +++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/difftastic/differ.rb b/lib/difftastic/differ.rb index 4ab8ead..b352c1c 100644 --- a/lib/difftastic/differ.rb +++ b/lib/difftastic/differ.rb @@ -10,7 +10,7 @@ class Difftastic::Differ MAX_ITEMS_INCREMENT = 10 DIFF_UNAVAILABLE_MESSAGE = "[Diff unavailable: exceeded depth/size display limits]" - def initialize(background: nil, color: nil, syntax_highlight: nil, context: nil, width: nil, tab_width: nil, parse_error_limit: nil, underline_highlights: true, left_label: nil, right_label: nil, display: "side-by-side-show-both", max_depth: nil, max_items: nil, max_depth_cap: nil, max_items_cap: nil) + def initialize(background: nil, color: nil, syntax_highlight: nil, context: nil, width: nil, tab_width: nil, parse_error_limit: nil, underline_highlights: true, left_label: nil, right_label: nil, display: "side-by-side-show-both", max_depth: nil, max_items: nil, max_depth_cap: nil, max_items_cap: nil, max_depth_increment: nil, max_items_increment: nil) @show_paths = false @background = background => :dark | :light | nil @color = color => :always | :never | :auto | nil @@ -27,6 +27,8 @@ def initialize(background: nil, color: nil, syntax_highlight: nil, context: nil, @max_items = max_items => Integer | nil @max_depth_cap = max_depth_cap => Integer | nil @max_items_cap = max_items_cap => Integer | nil + @max_depth_increment = max_depth_increment => Integer | nil + @max_items_increment = max_items_increment => Integer | nil end def diff_objects(old, new) @@ -35,13 +37,15 @@ def diff_objects(old, new) max_items = @max_items || DEFAULT_MAX_ITEMS max_depth_cap = @max_depth_cap || DEFAULT_MAX_DEPTH_CAP max_items_cap = @max_items_cap || DEFAULT_MAX_ITEMS_CAP + max_depth_increment = @max_depth_increment || MAX_DEPTH_INCREMENT + max_items_increment = @max_items_increment || MAX_ITEMS_INCREMENT loop do old_str = Difftastic.pretty(old, tab_width:, max_depth:, max_items:) new_str = Difftastic.pretty(new, tab_width:, max_depth:, max_items:) - # If prettified strings don't differ, the strings probably where truncated to max_depth and/or max_items - # PrettyPlease then correctly did not return un-matching strings + # If prettified strings don't differ, the strings probably were truncated to max_depth and/or max_items + # PrettyPlease then correctly did not return non-matching strings # In that case we increase max_depth & max_items in the loop # until DEFAULT_MAX_ITEMS_CAP or DEFAULT_MAX_DEPTH_CAP is exceeded if old_str != new_str @@ -54,9 +58,9 @@ def diff_objects(old, new) end # Increase limits and retry, while never increasing to more than max_depth_cap/max_items_cap - # Both values are matched to perform 3 retries, but neither value would increase to more than its max anyways - max_depth = [max_depth + MAX_DEPTH_INCREMENT, max_depth_cap].min - max_items = [max_items + MAX_ITEMS_INCREMENT, max_items_cap].min + # Both values are matched to perform 3 retries, but neither value would increase beyond its cap anyway + max_depth = [max_depth + max_depth_increment, max_depth_cap].min + max_items = [max_items + max_items_increment, max_items_cap].min end end diff --git a/test/diff_objects_adaptive_test.rb b/test/diff_objects_adaptive_test.rb index 893b077..4064a96 100644 --- a/test/diff_objects_adaptive_test.rb +++ b/test/diff_objects_adaptive_test.rb @@ -7,12 +7,15 @@ class DiffObjectsAdaptiveTest < Minitest::Spec # The adaptive logic works the same regardless of the actual values. TEST_MAX_DEPTH = 3 TEST_MAX_ITEMS = 5 + TEST_INCREMENT = 1 def differ(**options) Difftastic::Differ.new( color: :never, max_depth: TEST_MAX_DEPTH, max_items: TEST_MAX_ITEMS, + max_depth_increment: TEST_INCREMENT, + max_items_increment: TEST_INCREMENT, **options # overrides defaults when provided ) end From 16985ac2039bb56cb541a6263970013fe7e81af1 Mon Sep 17 00:00:00 2001 From: Paul W <167196940+top-sigrid@users.noreply.github.com> Date: Sat, 17 Jan 2026 21:57:42 +0100 Subject: [PATCH 5/8] Change unavailable message assertions --- test/diff_objects_adaptive_test.rb | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/test/diff_objects_adaptive_test.rb b/test/diff_objects_adaptive_test.rb index 4064a96..356c6ce 100644 --- a/test/diff_objects_adaptive_test.rb +++ b/test/diff_objects_adaptive_test.rb @@ -8,6 +8,7 @@ class DiffObjectsAdaptiveTest < Minitest::Spec TEST_MAX_DEPTH = 3 TEST_MAX_ITEMS = 5 TEST_INCREMENT = 1 + DIFF_UNAVAILABLE_MESSAGE = Difftastic::Differ::DIFF_UNAVAILABLE_MESSAGE def differ(**options) Difftastic::Differ.new( @@ -41,7 +42,7 @@ def array_with_diff_at(position, value) output = differ.diff_objects(old, new) - refute_includes output, "No changes" + refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end @@ -53,7 +54,7 @@ def array_with_diff_at(position, value) output = differ.diff_objects(old, new) - refute_includes output, "No changes" + refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end @@ -65,7 +66,7 @@ def array_with_diff_at(position, value) output = differ.diff_objects(old, new) - refute_includes output, "No changes" + refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end @@ -78,7 +79,7 @@ def array_with_diff_at(position, value) output = differ.diff_objects(old, new) - refute_includes output, "No changes" + refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end @@ -90,7 +91,7 @@ def array_with_diff_at(position, value) output = differ.diff_objects(old, new) - refute_includes output, "No changes" + refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end @@ -102,7 +103,7 @@ def array_with_diff_at(position, value) output = differ.diff_objects(old, new) - refute_includes output, "No changes" + refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end @@ -116,7 +117,7 @@ def array_with_diff_at(position, value) output = differ(max_depth: 1).diff_objects(old, new) - refute_includes output, "No changes" + refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end @@ -128,7 +129,7 @@ def array_with_diff_at(position, value) output = differ(max_items: 1).diff_objects(old, new) - refute_includes output, "No changes" + refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end @@ -142,7 +143,7 @@ def array_with_diff_at(position, value) output = differ(max_depth_cap: depth + 1).diff_objects(old, new) - refute_includes output, "No changes" + refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end @@ -154,7 +155,7 @@ def array_with_diff_at(position, value) output = differ(max_items_cap: position + 1).diff_objects(old, new) - refute_includes output, "No changes" + refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end @@ -169,7 +170,7 @@ def array_with_diff_at(position, value) output = differ(max_depth_cap: cap).diff_objects(old, new) - assert_includes output, Difftastic::Differ::DIFF_UNAVAILABLE_MESSAGE + assert_includes output, DIFF_UNAVAILABLE_MESSAGE end it "returns unavailable message when position exceeds max_items_cap" do @@ -180,7 +181,7 @@ def array_with_diff_at(position, value) output = differ(max_items_cap: cap).diff_objects(old, new) - assert_includes output, Difftastic::Differ::DIFF_UNAVAILABLE_MESSAGE + assert_includes output, DIFF_UNAVAILABLE_MESSAGE end end end From 3622986a2762a4f0472d8844b6ac3fd80271fe70 Mon Sep 17 00:00:00 2001 From: Paul W <167196940+top-sigrid@users.noreply.github.com> Date: Sun, 18 Jan 2026 08:59:42 +0100 Subject: [PATCH 6/8] Set caps in test lower --- test/diff_objects_adaptive_test.rb | 47 ++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/test/diff_objects_adaptive_test.rb b/test/diff_objects_adaptive_test.rb index 356c6ce..79ab1a1 100644 --- a/test/diff_objects_adaptive_test.rb +++ b/test/diff_objects_adaptive_test.rb @@ -7,6 +7,8 @@ class DiffObjectsAdaptiveTest < Minitest::Spec # The adaptive logic works the same regardless of the actual values. TEST_MAX_DEPTH = 3 TEST_MAX_ITEMS = 5 + TEST_MAX_DEPTH_CAP = 10 + TEST_MAX_ITEMS_CAP = 20 TEST_INCREMENT = 1 DIFF_UNAVAILABLE_MESSAGE = Difftastic::Differ::DIFF_UNAVAILABLE_MESSAGE @@ -15,6 +17,8 @@ def differ(**options) color: :never, max_depth: TEST_MAX_DEPTH, max_items: TEST_MAX_ITEMS, + max_depth_cap: TEST_MAX_DEPTH_CAP, + max_items_cap: TEST_MAX_ITEMS_CAP, max_depth_increment: TEST_INCREMENT, max_items_increment: TEST_INCREMENT, **options # overrides defaults when provided @@ -24,8 +28,12 @@ def differ(**options) # Example: # nested_hash(4, "x") # # => { l1: { l2: { l3: { l4: "x" } } } } - def nested_hash(depth, leaf_value) - (1..depth).reverse_each.reduce(leaf_value) { |inner, i| { "l#{i}": inner } } + def nested_hash(depth, nested_value) + (1..depth) + .reverse_each + .reduce(nested_value) do |inner, i| + { "l#{i}": inner } + end end # Example: @@ -148,6 +156,17 @@ def array_with_diff_at(position, value) assert_includes output, "new" end + it "returns unavailable when depth exceeds custom max_depth_cap" do + cap = 2 + old = nested_hash(cap + 1, "old") + new = nested_hash(cap + 1, "new") + + # Both caps must be hit (&&), and starting max must be smaller than cap to trigger truncation + output = differ(max_depth: 1, max_depth_cap: cap, max_items_cap: 1).diff_objects(old, new) + + assert_includes output, DIFF_UNAVAILABLE_MESSAGE + end + it "respects custom max_items_cap" do position = TEST_MAX_ITEMS + 5 old = array_with_diff_at(position, "old") @@ -159,6 +178,30 @@ def array_with_diff_at(position, value) assert_includes output, "old" assert_includes output, "new" end + + it "returns unavailable when position exceeds custom max_items_cap" do + cap = 2 + old = array_with_diff_at(cap + 1, "old") + new = array_with_diff_at(cap + 1, "new") + + # Both caps must be hit (&&), and starting max must be smaller than cap to trigger truncation + output = differ(max_items: 1, max_depth_cap: 1, max_items_cap: cap).diff_objects(old, new) + + assert_includes output, DIFF_UNAVAILABLE_MESSAGE + end + + it "uses starting max when higher than cap without adaptation" do + cap = 2 + old = array_with_diff_at(cap + 1, "old") + new = array_with_diff_at(cap + 1, "new") + + # With max_items: 5 (default) > cap: 2, no truncation occurs, diff found immediately + output = differ(max_items_cap: cap).diff_objects(old, new) + + refute_includes output, DIFF_UNAVAILABLE_MESSAGE + assert_includes output, "old" + assert_includes output, "new" + end end describe "loop termination at caps" do From d0f08eaa9ca8c1e091694bd352d251024f9d8a32 Mon Sep 17 00:00:00 2001 From: Paul W <167196940+top-sigrid@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:37:44 +0100 Subject: [PATCH 7/8] test cleanup --- test/diff_objects_adaptive_test.rb | 75 ++++++++++++++++++------------ 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/test/diff_objects_adaptive_test.rb b/test/diff_objects_adaptive_test.rb index 79ab1a1..18b7a7c 100644 --- a/test/diff_objects_adaptive_test.rb +++ b/test/diff_objects_adaptive_test.rb @@ -8,7 +8,7 @@ class DiffObjectsAdaptiveTest < Minitest::Spec TEST_MAX_DEPTH = 3 TEST_MAX_ITEMS = 5 TEST_MAX_DEPTH_CAP = 10 - TEST_MAX_ITEMS_CAP = 20 + TEST_MAX_ITEMS_CAP = 12 TEST_INCREMENT = 1 DIFF_UNAVAILABLE_MESSAGE = Difftastic::Differ::DIFF_UNAVAILABLE_MESSAGE @@ -119,7 +119,7 @@ def array_with_diff_at(position, value) describe "configurable starting values" do it "respects custom max_depth" do - depth = TEST_MAX_DEPTH - 2 + depth = TEST_MAX_DEPTH_CAP - 1 old = nested_hash(depth, "old") new = nested_hash(depth, "new") @@ -131,7 +131,7 @@ def array_with_diff_at(position, value) end it "respects custom max_items" do - position = TEST_MAX_ITEMS - 2 + position = TEST_MAX_ITEMS_CAP - 1 old = array_with_diff_at(position, "old") new = array_with_diff_at(position, "new") @@ -145,84 +145,99 @@ def array_with_diff_at(position, value) describe "configurable caps" do it "respects custom max_depth_cap" do - depth = TEST_MAX_DEPTH + 3 + depth = TEST_MAX_DEPTH - 1 old = nested_hash(depth, "old") new = nested_hash(depth, "new") - output = differ(max_depth_cap: depth + 1).diff_objects(old, new) + output = differ(max_depth_cap: TEST_MAX_DEPTH).diff_objects(old, new) refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end - it "returns unavailable when depth exceeds custom max_depth_cap" do - cap = 2 - old = nested_hash(cap + 1, "old") - new = nested_hash(cap + 1, "new") - - # Both caps must be hit (&&), and starting max must be smaller than cap to trigger truncation - output = differ(max_depth: 1, max_depth_cap: cap, max_items_cap: 1).diff_objects(old, new) - - assert_includes output, DIFF_UNAVAILABLE_MESSAGE - end - it "respects custom max_items_cap" do - position = TEST_MAX_ITEMS + 5 + position = TEST_MAX_ITEMS - 1 old = array_with_diff_at(position, "old") new = array_with_diff_at(position, "new") - output = differ(max_items_cap: position + 1).diff_objects(old, new) + output = differ(max_items_cap: TEST_MAX_ITEMS).diff_objects(old, new) refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end + it "returns unavailable when depth exceeds custom max_depth_cap" do + cap = 2 + old = nested_hash(cap, "old") + new = nested_hash(cap, "new") + + # Both caps must be hit (&&), and starting max must be smaller than cap to trigger truncation + output = differ(max_depth: 1, max_depth_cap: cap, max_items_cap: 1).diff_objects(old, new) + + assert_includes output, DIFF_UNAVAILABLE_MESSAGE + refute_includes output, "old" + refute_includes output, "new" + end + it "returns unavailable when position exceeds custom max_items_cap" do cap = 2 - old = array_with_diff_at(cap + 1, "old") - new = array_with_diff_at(cap + 1, "new") + old = array_with_diff_at(cap, "old") + new = array_with_diff_at(cap, "new") # Both caps must be hit (&&), and starting max must be smaller than cap to trigger truncation output = differ(max_items: 1, max_depth_cap: 1, max_items_cap: cap).diff_objects(old, new) assert_includes output, DIFF_UNAVAILABLE_MESSAGE + refute_includes output, "old" + refute_includes output, "new" end - it "uses starting max when higher than cap without adaptation" do + it "uses starting max_items when higher than cap without adaptation" do cap = 2 - old = array_with_diff_at(cap + 1, "old") - new = array_with_diff_at(cap + 1, "new") + old = array_with_diff_at(cap, "old") + new = array_with_diff_at(cap, "new") - # With max_items: 5 (default) > cap: 2, no truncation occurs, diff found immediately + # With max_items: 5 (default) > max_items_cap: 2, no truncation occurs, diff found immediately output = differ(max_items_cap: cap).diff_objects(old, new) refute_includes output, DIFF_UNAVAILABLE_MESSAGE assert_includes output, "old" assert_includes output, "new" end + + it "uses starting max_depth when higher than cap without adaptation" do + cap = 2 + old = nested_hash(cap, "old") + new = nested_hash(cap, "new") + + # With max_depth: 3 (default) > max_depth_cap: 2, no truncation occurs, diff found immediately + output = differ(max_depth_cap: cap).diff_objects(old, new) + + refute_includes output, DIFF_UNAVAILABLE_MESSAGE + assert_includes output, "old" + assert_includes output, "new" + end end describe "loop termination at caps" do it "returns unavailable message when depth exceeds max_depth_cap" do - cap = TEST_MAX_DEPTH + 2 - depth = cap + 1 + depth = TEST_MAX_DEPTH_CAP + 1 old = nested_hash(depth, "old") new = nested_hash(depth, "new") - output = differ(max_depth_cap: cap).diff_objects(old, new) + output = differ.diff_objects(old, new) assert_includes output, DIFF_UNAVAILABLE_MESSAGE end it "returns unavailable message when position exceeds max_items_cap" do - cap = TEST_MAX_ITEMS + 5 - position = cap + 1 + position = TEST_MAX_ITEMS_CAP + 1 old = array_with_diff_at(position, "old") new = array_with_diff_at(position, "new") - output = differ(max_items_cap: cap).diff_objects(old, new) + output = differ.diff_objects(old, new) assert_includes output, DIFF_UNAVAILABLE_MESSAGE end From c8631c94ea0f0b1e4a2556b7edb363c75a9103ab Mon Sep 17 00:00:00 2001 From: Paul W <167196940+top-sigrid@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:08:07 +0100 Subject: [PATCH 8/8] cleanup tests and comments --- lib/difftastic/differ.rb | 11 +++++------ test/diff_objects_adaptive_test.rb | 4 ++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/difftastic/differ.rb b/lib/difftastic/differ.rb index b352c1c..47758ad 100644 --- a/lib/difftastic/differ.rb +++ b/lib/difftastic/differ.rb @@ -44,10 +44,9 @@ def diff_objects(old, new) old_str = Difftastic.pretty(old, tab_width:, max_depth:, max_items:) new_str = Difftastic.pretty(new, tab_width:, max_depth:, max_items:) - # If prettified strings don't differ, the strings probably were truncated to max_depth and/or max_items - # PrettyPlease then correctly did not return non-matching strings - # In that case we increase max_depth & max_items in the loop - # until DEFAULT_MAX_ITEMS_CAP or DEFAULT_MAX_DEPTH_CAP is exceeded + # If prettified strings don't differ, the strings probably were truncated to max_depth and/or max_items. + # PrettyPlease then correctly did not return non-matching strings. + # In that case we increase max_depth & max_items in the loop up to DEFAULT_MAX_ITEMS_CAP or DEFAULT_MAX_DEPTH_CAP. if old_str != new_str return diff_strings(old_str, new_str, file_extension: "rb") end @@ -57,8 +56,8 @@ def diff_objects(old, new) return DIFF_UNAVAILABLE_MESSAGE end - # Increase limits and retry, while never increasing to more than max_depth_cap/max_items_cap - # Both values are matched to perform 3 retries, but neither value would increase beyond its cap anyway + # Increase limits and retry, while never increasing to more than max_depth_cap/max_items_cap. + # Both values are matched to perform 3 retries, but neither value would increase beyond its cap anyway. max_depth = [max_depth + max_depth_increment, max_depth_cap].min max_items = [max_items + max_items_increment, max_items_cap].min end diff --git a/test/diff_objects_adaptive_test.rb b/test/diff_objects_adaptive_test.rb index 18b7a7c..dd8ed28 100644 --- a/test/diff_objects_adaptive_test.rb +++ b/test/diff_objects_adaptive_test.rb @@ -230,6 +230,8 @@ def array_with_diff_at(position, value) output = differ.diff_objects(old, new) assert_includes output, DIFF_UNAVAILABLE_MESSAGE + refute_includes output, "old" + refute_includes output, "new" end it "returns unavailable message when position exceeds max_items_cap" do @@ -240,6 +242,8 @@ def array_with_diff_at(position, value) output = differ.diff_objects(old, new) assert_includes output, DIFF_UNAVAILABLE_MESSAGE + refute_includes output, "old" + refute_includes output, "new" end end end