From ee2b40ccf119759095597d59ee72fdf2e0586806 Mon Sep 17 00:00:00 2001 From: nick evans Date: Mon, 28 Apr 2025 14:16:45 -0400 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`SequenceSet#slice`=20?= =?UTF-8?q?with=20range=20`(start...0)`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bug was that exclusive ranges ending in zero would be converted to end on `-1`, which would be interpretted as the last value in the range. --- lib/net/imap/sequence_set.rb | 5 ++++- test/net/imap/test_sequence_set.rb | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/net/imap/sequence_set.rb b/lib/net/imap/sequence_set.rb index cbdc2a74..8aa6187f 100644 --- a/lib/net/imap/sequence_set.rb +++ b/lib/net/imap/sequence_set.rb @@ -1245,7 +1245,10 @@ def slice_length(start, length) def slice_range(range) first = range.begin || 0 last = range.end || -1 - last -= 1 if range.exclude_end? && range.end && last != STAR_INT + if range.exclude_end? + return SequenceSet.empty if last.zero? + last -= 1 if range.end && last != STAR_INT + end if (first * last).positive? && last < first SequenceSet.empty elsif (min = at(first)) diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb index f9f43145..5dd90b75 100644 --- a/test/net/imap/test_sequence_set.rb +++ b/test/net/imap/test_sequence_set.rb @@ -286,6 +286,8 @@ def obj.to_sequence_set; 192_168.001_255 end assert_equal SequenceSet.empty, SequenceSet[1..100][-50..-60] assert_equal SequenceSet.empty, SequenceSet[1..100][-10..10] assert_equal SequenceSet.empty, SequenceSet[1..100][60..-60] + assert_equal SequenceSet.empty, SequenceSet[1..100][10...0] + assert_equal SequenceSet.empty, SequenceSet[1..100][0...0] assert_nil SequenceSet.empty[2..4] assert_nil SequenceSet[101..200][1000..1060] assert_nil SequenceSet[101..200][-1000..-60] From 14d698e9d2b1815c0b0bc699d6a6f371a1de7082 Mon Sep 17 00:00:00 2001 From: nick evans Date: Mon, 28 Apr 2025 16:47:58 -0400 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=90=9B=20Fix=20inconsistently=20froze?= =?UTF-8?q?n=20SequenceSet#[]=20result?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This maybe isn't actually documented very well (or at all...) but most SequenceSet transform methods return a frozen result when +self+ is frozen and a mutable result when +self+ is mutable. Except +limit+ which always returns a frozen result. And (before this commit) +slice+, which inconsistently returned with matching frozen status when the result wasn't empty, but always returned a frozen set when the result _was_ empty. Adding these tests exposed a much more significant bug: `SequenceSet#xor` mutates the reciever. --- lib/net/imap/sequence_set.rb | 7 ++++--- test/net/imap/test_sequence_set.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/net/imap/sequence_set.rb b/lib/net/imap/sequence_set.rb index 8aa6187f..4fc2892e 100644 --- a/lib/net/imap/sequence_set.rb +++ b/lib/net/imap/sequence_set.rb @@ -1246,16 +1246,16 @@ def slice_range(range) first = range.begin || 0 last = range.end || -1 if range.exclude_end? - return SequenceSet.empty if last.zero? + return remain_frozen_empty if last.zero? last -= 1 if range.end && last != STAR_INT end if (first * last).positive? && last < first - SequenceSet.empty + remain_frozen_empty elsif (min = at(first)) max = at(last) if max == :* then self & (min..) elsif min <= max then self & (min..max) - else SequenceSet.empty + else remain_frozen_empty end end end @@ -1383,6 +1383,7 @@ def send_data(imap, tag) # :nodoc: private def remain_frozen(set) frozen? ? set.freeze : set end + def remain_frozen_empty; frozen? ? SequenceSet.empty : SequenceSet.new end # frozen clones are shallow copied def initialize_clone(other) diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb index 5dd90b75..9f8b762f 100644 --- a/test/net/imap/test_sequence_set.rb +++ b/test/net/imap/test_sequence_set.rb @@ -83,6 +83,32 @@ def compare_to_reference_set(nums, set, seqset) end end + data "#slice(length)", {transform: ->{ _1.slice(0, 10) }, } + data "#slice(range)", {transform: ->{ _1.slice(0...10) }, } + data "#slice => empty", {transform: ->{ _1.slice(0...0) }, } + data "#slice => empty", {transform: ->{ _1.slice(10..9) }, } + data "#union", {transform: ->{ _1 | (1..100) }, } + data "#intersection", {transform: ->{ _1 & (1..100) }, } + data "#difference", {transform: ->{ _1 - (1..100) }, } + # data "#xor", {transform: ->{ _1 ^ (1..100) }, } + data "#complement", {transform: ->{ ~_1 }, } + data "#normalize", {transform: ->{ _1.normalize }, } + data "#limit", {transform: ->{ _1.limit(max: 22) }, freeze: :always } + data "#limit => empty", {transform: ->{ _1.limit(max: 1) }, freeze: :always } + test "transforms keep frozen status" do |data| + data => {transform:} + set = SequenceSet.new("2:4,7:11,99,999") + result = transform.to_proc.(set) + if data in {freeze: :always} + assert result.frozen?, "this transform always returns frozen" + else + refute result.frozen?, "transform of non-frozen returned frozen" + end + set.freeze + result = transform.to_proc.(set) + assert result.frozen?, "transform of frozen returned non-frozen" + end + %i[clone dup].each do |method| test "##{method}" do orig = SequenceSet.new "2:4,7:11,99,999" From 33367bb888d11b65e06db47f261ea051ae45e7fe Mon Sep 17 00:00:00 2001 From: nick evans Date: Mon, 28 Apr 2025 16:59:56 -0400 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=90=9B=20`SequenceSet#xor`=20should?= =?UTF-8?q?=20not=20modify=20`self`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `set ^ other` and `set.xor other` are supposed to be safe transforms. But, unfortunately, they modified the receiver if it wasn't frozen, and crashed when it was! The fix is trivial: convert `self` to `dup`. --- lib/net/imap/sequence_set.rb | 2 +- test/net/imap/test_sequence_set.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/net/imap/sequence_set.rb b/lib/net/imap/sequence_set.rb index 4fc2892e..2748aa4b 100644 --- a/lib/net/imap/sequence_set.rb +++ b/lib/net/imap/sequence_set.rb @@ -676,7 +676,7 @@ def &(other) # # (seqset ^ other) is equivalent to ((seqset | other) - # (seqset & other)). - def ^(other) remain_frozen (self | other).subtract(self & other) end + def ^(other) remain_frozen (dup | other).subtract(self & other) end alias xor :^ # :call-seq: diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb index 9f8b762f..7edc31bf 100644 --- a/test/net/imap/test_sequence_set.rb +++ b/test/net/imap/test_sequence_set.rb @@ -90,7 +90,7 @@ def compare_to_reference_set(nums, set, seqset) data "#union", {transform: ->{ _1 | (1..100) }, } data "#intersection", {transform: ->{ _1 & (1..100) }, } data "#difference", {transform: ->{ _1 - (1..100) }, } - # data "#xor", {transform: ->{ _1 ^ (1..100) }, } + data "#xor", {transform: ->{ _1 ^ (1..100) }, } data "#complement", {transform: ->{ ~_1 }, } data "#normalize", {transform: ->{ _1.normalize }, } data "#limit", {transform: ->{ _1.limit(max: 22) }, freeze: :always } @@ -98,7 +98,9 @@ def compare_to_reference_set(nums, set, seqset) test "transforms keep frozen status" do |data| data => {transform:} set = SequenceSet.new("2:4,7:11,99,999") + dup = set.dup result = transform.to_proc.(set) + assert_equal dup, set, "transform should not modified" if data in {freeze: :always} assert result.frozen?, "this transform always returns frozen" else From da1390c374efd33f049d25e3720ffa9991c2dcf6 Mon Sep 17 00:00:00 2001 From: nick evans Date: Mon, 28 Apr 2025 17:27:18 -0400 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=90=9B=20Fix=20SequenceSet#slice=20wh?= =?UTF-8?q?en=20length=20>=20result=20size?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The goal is for `#[]` (aliased as `#slice`) to behave similarly to `Array#[]`/`Array#slice`. When `Array#slice` has a length or range that extends beyond the end of the array, they simply return everything up to the end. --- lib/net/imap/sequence_set.rb | 1 + test/net/imap/test_sequence_set.rb | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/lib/net/imap/sequence_set.rb b/lib/net/imap/sequence_set.rb index 2748aa4b..568e2a4c 100644 --- a/lib/net/imap/sequence_set.rb +++ b/lib/net/imap/sequence_set.rb @@ -1253,6 +1253,7 @@ def slice_range(range) remain_frozen_empty elsif (min = at(first)) max = at(last) + max = :* if max.nil? if max == :* then self & (min..) elsif min <= max then self & (min..max) else remain_frozen_empty diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb index 7edc31bf..b3490d77 100644 --- a/test/net/imap/test_sequence_set.rb +++ b/test/net/imap/test_sequence_set.rb @@ -293,6 +293,9 @@ def obj.to_sequence_set; 192_168.001_255 end SequenceSet[((1..10_000) % 10).to_a][-5, 4] assert_nil SequenceSet[111..222, 888..999][2000, 4] assert_nil SequenceSet[111..222, 888..999][-2000, 4] + # with length longer than the remaining members + assert_equal SequenceSet[101...200], + SequenceSet[1...200][100, 10000] end test "#[range]" do @@ -319,6 +322,8 @@ def obj.to_sequence_set; 192_168.001_255 end assert_nil SequenceSet.empty[2..4] assert_nil SequenceSet[101..200][1000..1060] assert_nil SequenceSet[101..200][-1000..-60] + # with length longer than the remaining members + assert_equal SequenceSet[101..1111], SequenceSet[1..1111][100..999_999] end test "#find_index" do From 06fb07173c976d60cd554dfdf71adf8e47dcf42e Mon Sep 17 00:00:00 2001 From: nick evans Date: Mon, 28 Apr 2025 23:22:13 -0400 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=85=20Update=20backport=20for=20ruby?= =?UTF-8?q?=202.7=20compatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/net/imap/test_sequence_set.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb index b3490d77..bbc22f26 100644 --- a/test/net/imap/test_sequence_set.rb +++ b/test/net/imap/test_sequence_set.rb @@ -96,12 +96,12 @@ def compare_to_reference_set(nums, set, seqset) data "#limit", {transform: ->{ _1.limit(max: 22) }, freeze: :always } data "#limit => empty", {transform: ->{ _1.limit(max: 1) }, freeze: :always } test "transforms keep frozen status" do |data| - data => {transform:} + transform = data.fetch(:transform) set = SequenceSet.new("2:4,7:11,99,999") dup = set.dup result = transform.to_proc.(set) assert_equal dup, set, "transform should not modified" - if data in {freeze: :always} + if data[:freeze] == :always assert result.frozen?, "this transform always returns frozen" else refute result.frozen?, "transform of non-frozen returned frozen" From 4b3270c0ae64a809f3160b33a23d0dd254dc460a Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 29 Apr 2025 10:31:39 -0400 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=85=20Add=20tests=20for=20`SequenceSe?= =?UTF-8?q?t#xor`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I'm not sure how this escaped testing before, but yikes! --- test/net/imap/test_sequence_set.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb index bbc22f26..9d22c4a9 100644 --- a/test/net/imap/test_sequence_set.rb +++ b/test/net/imap/test_sequence_set.rb @@ -573,6 +573,16 @@ def obj.to_sequence_set; 192_168.001_255 end assert_equal seqset["1,5,11:99"], seqset["1,5:6,8:9,11:99"].subtract("6:9") end + test "#xor" do + seqset = -> { SequenceSet.new(_1) } + assert_equal seqset["1:5,11:15"], seqset["1:10"] ^ seqset["6:15"] + assert_equal seqset["1,3,5:6"], seqset[1..5] ^ [2, 4, 6] + assert_equal SequenceSet.empty, seqset[1..5] ^ seqset[1..5] + assert_equal seqset["1:100"], seqset["1:50"] ^ seqset["51:100"] + assert_equal seqset["1:50"], seqset["1:50"] ^ SequenceSet.empty + assert_equal seqset["1:50"], SequenceSet.empty ^ seqset["1:50"] + end + test "#min" do assert_equal 3, SequenceSet.new("34:3").min assert_equal 345, SequenceSet.new("345,678").min From 45e39d1df0d6ba95c86008de5486f611f08aa111 Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 29 Apr 2025 12:53:08 -0400 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9C=85=20Add=20basic=20fuzz=20tests=20fo?= =?UTF-8?q?r=20SequenceSet=20operators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I almost accidentally committed a big bug for `#xor`. Yikes! I decided to take that opportunity to simply add some randomized tests on all of the set operators, based on set identities that should always hold true. These can also be used for microbenchmarks and profiling of SequenceSet. --- test/net/imap/test_sequence_set.rb | 78 +++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/test/net/imap/test_sequence_set.rb b/test/net/imap/test_sequence_set.rb index 9d22c4a9..81c4428e 100644 --- a/test/net/imap/test_sequence_set.rb +++ b/test/net/imap/test_sequence_set.rb @@ -23,7 +23,7 @@ def compare_to_reference_set(nums, set, seqset) assert seqset.cover? sorted.sample 100 end - test "compared to reference Set, add many random values" do + test "fuzz test: add numbers and compare to reference Set" do set = Set.new seqset = SequenceSet.new 10.times do @@ -32,7 +32,7 @@ def compare_to_reference_set(nums, set, seqset) end end - test "compared to reference Set, add many large ranges" do + test "fuzz test: add ranges and compare to reference Set" do set = Set.new seqset = SequenceSet.new (1..10_000).each_slice(250) do @@ -41,6 +41,80 @@ def compare_to_reference_set(nums, set, seqset) end end + test "fuzz test: set union identities" do + 10.times do + lhs = SequenceSet[Array.new(100) { rand(1..300) }] + rhs = SequenceSet[Array.new(100) { rand(1..300) }] + union = lhs | rhs + assert_equal union, rhs | lhs # commutative + assert_equal union, ~(~lhs & ~rhs) # De Morgan's Law + assert_equal union, lhs | (lhs ^ rhs) + assert_equal union, lhs | (rhs - lhs) + assert_equal union, (lhs & rhs) ^ (lhs ^ rhs) + mutable = lhs.dup + assert_equal union, mutable.merge(rhs) + assert_equal union, mutable + end + end + + test "fuzz test: set intersection identities" do + 10.times do + lhs = SequenceSet[Array.new(100) { rand(1..300) }] + rhs = SequenceSet[Array.new(100) { rand(1..300) }] + intersection = lhs & rhs + assert_equal intersection, rhs & lhs # commutative + assert_equal intersection, ~(~lhs | ~rhs) # De Morgan's Law + assert_equal intersection, lhs - ~rhs + assert_equal intersection, lhs - (lhs - rhs) + assert_equal intersection, lhs - (lhs ^ rhs) + assert_equal intersection, lhs ^ (lhs - rhs) + end + end + + test "fuzz test: set subtraction identities" do + 10.times do + lhs = SequenceSet[Array.new(100) { rand(1..300) }] + rhs = SequenceSet[Array.new(100) { rand(1..300) }] + difference = lhs - rhs + assert_equal difference, ~rhs - ~lhs + assert_equal difference, ~(~lhs | rhs) + assert_equal difference, lhs & (lhs ^ rhs) + assert_equal difference, lhs ^ (lhs & rhs) + assert_equal difference, rhs ^ (lhs | rhs) + mutable = lhs.dup + assert_equal difference, mutable.subtract(rhs) + assert_equal difference, mutable + end + end + + test "fuzz test: set xor identities" do + 10.times do + lhs = SequenceSet[Array.new(100) { rand(1..300) }] + rhs = SequenceSet[Array.new(100) { rand(1..300) }] + mid = SequenceSet[Array.new(100) { rand(1..300) }] + xor = lhs ^ rhs + assert_equal xor, rhs ^ lhs # commutative + assert_equal xor, (lhs | rhs) - (lhs & rhs) + assert_equal xor, (lhs ^ mid) ^ (mid ^ rhs) + assert_equal xor, ~lhs ^ ~rhs + end + end + + test "fuzz test: set complement identities" do + 10.times do + set = SequenceSet[Array.new(100) { rand(1..300) }] + complement = ~set + assert_equal set, ~complement + assert_equal complement, ~set.dup + assert_equal complement, SequenceSet.full - set + mutable = set.dup + assert_equal complement, mutable.complement! + assert_equal complement, mutable + assert_equal set, mutable.complement! + assert_equal set, mutable + end + end + test "#== equality by value (not by identity or representation)" do assert_equal SequenceSet.new, SequenceSet.new assert_equal SequenceSet.new("1"), SequenceSet[1] From 644a5d1be75c4a8922bf4dcb8ac239719d8ba514 Mon Sep 17 00:00:00 2001 From: nick evans Date: Tue, 29 Apr 2025 13:29:42 -0400 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=94=96=20Bump=20version=20to=200.4.21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/net/imap.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/imap.rb b/lib/net/imap.rb index 8215cdb4..84bc0946 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -761,7 +761,7 @@ module Net # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml] # class IMAP < Protocol - VERSION = "0.4.20" + VERSION = "0.4.21" # Aliases for supported capabilities, to be used with the #enable command. ENABLE_ALIASES = {