Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add #extract_responses method #330

Merged
merged 3 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ module Net
# pre-authenticated connection.
# - #responses: Yields unhandled UntaggedResponse#data and <em>non-+nil+</em>
# ResponseCode#data.
# - #extract_responses: Removes and returns the responses for which the block
# returns a true value.
# - #clear_responses: Deletes unhandled data from #responses and returns it.
# - #add_response_handler: Add a block to be called inside the receiver thread
# with every server response.
Expand Down Expand Up @@ -2534,7 +2536,7 @@ def idle_done
# return the TaggedResponse directly, #add_response_handler must be used to
# handle all response codes.
#
# Related: #clear_responses, #response_handlers, #greeting
# Related: #extract_responses, #clear_responses, #response_handlers, #greeting
def responses(type = nil)
if block_given?
synchronize { yield(type ? @responses[type.to_s.upcase] : @responses) }
Expand Down Expand Up @@ -2562,7 +2564,7 @@ def responses(type = nil)
# Clearing responses is synchronized with other threads. The lock is
# released before returning.
#
# Related: #responses, #response_handlers
# Related: #extract_responses, #responses, #response_handlers
def clear_responses(type = nil)
synchronize {
if type
Expand All @@ -2576,6 +2578,30 @@ def clear_responses(type = nil)
.freeze
end

# :call-seq:
# extract_responses(type) {|response| ... } -> array
#
# Yields all of the unhandled #responses for a single response +type+.
# Removes and returns the responses for which the block returns a true
# value.
#
# Extracting responses is synchronized with other threads. The lock is
# released before returning.
#
# Related: #responses, #clear_responses
def extract_responses(type)
type = String.try_convert(type) or
raise ArgumentError, "type must be a string"
raise ArgumentError, "must provide a block" unless block_given?
extracted = []
responses(type) do |all|
all.reject! do |response|
extracted << response if yield response
end
end
extracted
end

# Returns all response handlers, including those that are added internally
# by commands. Each response handler will be called with every new
# UntaggedResponse, TaggedResponse, and ContinuationRequest.
Expand Down
3 changes: 3 additions & 0 deletions test/net/imap/fake_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ def state; connection.state end
# See CommandRouter#on
def on(...) connection&.on(...) end

# See Connection#unsolicited
def unsolicited(...) @mutex.synchronize { connection&.unsolicited(...) } end

private

attr_reader :tcp_server, :connection
Expand Down
1 change: 1 addition & 0 deletions test/net/imap/fake_server/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def initialize(server, tcp_socket:)

def commands; state.commands end
def on(...) router.on(...) end
def unsolicited(...) writer.untagged(...) end

def run
writer.greeting
Expand Down
59 changes: 57 additions & 2 deletions test/net/imap/test_imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,7 @@ def test_enable
end
end

def test_responses
test "#responses" do
with_fake_server do |server, imap|
# responses available before SELECT/EXAMINE
assert_equal(%w[IMAP4REV1 NAMESPACE MOVE IDLE UTF8=ACCEPT],
Expand Down Expand Up @@ -1144,7 +1144,7 @@ def test_responses
end
end

def test_clear_responses
test "#clear_responses" do
with_fake_server do |server, imap|
resp = imap.select "INBOX"
assert_equal([Net::IMAP::TaggedResponse, "RUBY0001", "OK"],
Expand All @@ -1168,6 +1168,49 @@ def test_clear_responses
end
end

test "#extract_responses" do
with_fake_server do |server, imap|
resp = imap.select "INBOX"
assert_equal([Net::IMAP::TaggedResponse, "RUBY0001", "OK"],
[resp.class, resp.tag, resp.name])
# Need to send a string type and a block
assert_raise(ArgumentError) do imap.extract_responses { true } end
assert_raise(ArgumentError) do imap.extract_responses(nil) { true } end
assert_raise(ArgumentError) do imap.extract_responses("OK") end
# matching nothing
assert_equal([172], imap.responses("EXISTS", &:dup))
assert_equal([], imap.extract_responses("EXISTS") { String === _1 })
assert_equal([172], imap.responses("EXISTS", &:dup))
# matching everything
assert_equal([172], imap.responses("EXISTS", &:dup))
assert_equal([172], imap.extract_responses("EXISTS", &:even?))
assert_equal([], imap.responses("EXISTS", &:dup))
# matching some
server.unsolicited("101 FETCH (UID 1111 FLAGS (\\Seen))")
server.unsolicited("102 FETCH (UID 2222 FLAGS (\\Seen \\Flagged))")
server.unsolicited("103 FETCH (UID 3333 FLAGS (\\Deleted))")
wait_for_response_count(imap, type: "FETCH", count: 3)

result = imap.extract_responses("FETCH") { _1.flags.include?(:Flagged) }
assert_equal(
[
Net::IMAP::FetchData.new(
102, {"UID" => 2222, "FLAGS" => [:Seen, :Flagged]}
),
],
result,
)
assert_equal 2, imap.responses("FETCH", &:count)

result = imap.extract_responses("FETCH") { _1.flags.include?(:Deleted) }
assert_equal(
[Net::IMAP::FetchData.new(103, {"UID" => 3333, "FLAGS" => [:Deleted]})],
result
)
assert_equal 1, imap.responses("FETCH", &:count)
end
end

test "#select with condstore" do
with_fake_server do |server, imap|
imap.select "inbox", condstore: true
Expand Down Expand Up @@ -1423,4 +1466,16 @@ def create_tcp_server
def server_addr
Addrinfo.tcp("localhost", 0).ip_address
end

def wait_for_response_count(imap, type:, count:,
timeout: 0.5, interval: 0.001)
deadline = Time.now + timeout
loop do
current_count = imap.responses(type, &:size)
break :count if count <= current_count
break :deadline if deadline < Time.now
sleep interval
end
end

end