diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8eac4542..a27663f8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,13 +19,14 @@ jobs: matrix: ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} os: [ ubuntu-latest, macos-latest, windows-latest ] - experimental: [false] - exclude: - - { ruby: head, os: windows-latest } include: - - { ruby: head, os: windows-latest, experimental: true } + - { ruby: head, experimental: true } + # - { ruby: jruby, os: ubuntu-latest, experimental: true } + - { ruby: jruby-head, os: ubuntu-latest, experimental: true } + # - { ruby: truffleruby, os: ubuntu-latest, experimental: true } + - { ruby: truffleruby-head, os: ubuntu-latest, experimental: true } runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.experimental }} + continue-on-error: ${{ matrix.experimental || false }} timeout-minutes: 15 steps: - uses: actions/checkout@v4 @@ -40,7 +41,8 @@ jobs: timeout-minutes: 5 # _should_ finish in under a minute - uses: joshmfrankel/simplecov-check-action@main - if: matrix.os == 'ubuntu-latest' && github.event_name != 'pull_request' + if: ${{ matrix.os == 'ubuntu-latest' && github.event_name != 'pull_request' && + !startsWith(matrix.ruby, 'truffleruby') && !startsWith(matrix.ruby, 'jruby') }} with: check_job_name: "SimpleCov - ${{ matrix.ruby }}" minimum_suite_coverage: 90 diff --git a/lib/net/imap.rb b/lib/net/imap.rb index c9a1e66d..c72992a3 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -1116,8 +1116,8 @@ def tls_verified?; @tls_verified end # # Related: #logout, #logout! def disconnect + in_logout_state = try_state_logout? return if disconnected? - state_logout! begin begin # try to call SSL::SSLSocket#io. @@ -1131,11 +1131,15 @@ def disconnect rescue Exception => e @receiver_thread.raise(e) end - @receiver_thread.join synchronize do @sock.close end + @receiver_thread.join raise e if e + ensure + # Try again after shutting down the receiver thread. With no reciever + # left to wait for, any remaining locks should be _very_ brief. + state_logout! unless in_logout_state end # Returns true if disconnected from the server. @@ -3062,8 +3066,8 @@ def idle(timeout = nil, &response_handler) raise @exception || Net::IMAP::Error.new("connection closed") end ensure + remove_response_handler(response_handler) unless @receiver_thread_terminating - remove_response_handler(response_handler) put_string("DONE#{CRLF}") response = get_tagged_response(tag, "IDLE", idle_response_timeout) end @@ -3346,8 +3350,6 @@ def start_receiver_thread rescue Exception => ex @receiver_thread_exception = ex # don't exit the thread with an exception - ensure - state_logout! end end @@ -3429,6 +3431,8 @@ def receive_responses @idle_done_cond.signal end end + ensure + state_logout! end def get_tagged_response(tag, cmd, timeout = nil) @@ -3791,15 +3795,29 @@ def state_selected! end def state_unselected! - state_authenticated! if connection_state.to_sym == :selected + synchronize do + state_authenticated! if connection_state.to_sym == :selected + end end def state_logout! + return true if connection_state in [:logout, *] synchronize do + return true if connection_state in [:logout, *] @connection_state = ConnectionState::Logout.new end end + # don't wait to aqcuire the lock + def try_state_logout? + return true if connection_state in [:logout, *] + return false unless acquired_lock = mon_try_enter + state_logout! + true + ensure + mon_exit if acquired_lock + end + def sasl_adapter SASLAdapter.new(self, &method(:send_command_with_continuations)) end diff --git a/lib/net/imap/data_lite.rb b/lib/net/imap/data_lite.rb index 5b96c873..733bd946 100644 --- a/lib/net/imap/data_lite.rb +++ b/lib/net/imap/data_lite.rb @@ -24,10 +24,56 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. - module Net class IMAP - data_or_object = RUBY_VERSION >= "3.2.0" ? ::Data : Object + + # :nocov: + # Skip coverage: how much of the file runs depends on the engine version. + + # Test whether RUBY_ENGINE has sufficient support for ::Data. + # TODO: golf this down to the bare minimum that's failing. + data_or_object = ::Object + if defined?(::Data) && ::Data.respond_to?(:define) + begin + class TestData < ::Data + def self.YAML(data) + coder = Struct.new(:map).new(data) + data = self.allocate + data.init_with(coder) + data + end + def init_with(coder) initialize(**coder.map.transform_keys(&:to_sym)) end + def deconstruct; [:ok, *super] end + end + + class TestDataDefine < TestData.define(:str, :bool) + def initialize(str: nil, bool: nil) + str => String | nil; str = -str if str + bool => true | false | nil; bool = !!bool + super + end + end + + test_init = TestDataDefine.YAML({"str" => "str"}) + test_empty = TestData.define[] + + if test_init.deconstruct != [:ok, "str", false] + raise "subclassing misbehaves" + elsif test_empty.deconstruct != [:ok] + raise "can't define empty" + end + data_or_object = ::Data + rescue => ex + warn "Insufficient implementation of Data: %s (%s) for %s %s" % [ + ex, ex.class, RUBY_ENGINE, RUBY_ENGINE_VERSION, + ] + data_or_object = ::Object + ensure + remove_const :TestData if const_defined?(:TestData) + remove_const :TestDataDefine if const_defined?(:TestDataDefine) + end + end + class DataLite < data_or_object def encode_with(coder) coder.map = to_h.transform_keys(&:to_s) end def init_with(coder) initialize(**coder.map.transform_keys(&:to_sym)) end @@ -37,9 +83,7 @@ def init_with(coder) initialize(**coder.map.transform_keys(&:to_sym)) end end end -# :nocov: -# Need to skip test coverage for the rest, because it isn't loaded by ruby 3.2+. -return if RUBY_VERSION >= "3.2.0" +return unless Net::IMAP::DataLite.superclass == Object module Net class IMAP diff --git a/test/lib/helper.rb b/test/lib/helper.rb index 3128c533..a4f53939 100644 --- a/test/lib/helper.rb +++ b/test/lib/helper.rb @@ -1,31 +1,34 @@ -require "simplecov" - -# Cannot use ".simplecov" file: simplecov-json triggers a circular require. -require "simplecov-json" -SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([ - SimpleCov::Formatter::HTMLFormatter, - SimpleCov::Formatter::JSONFormatter, -]) - -SimpleCov.start do - command_name "Net::IMAP tests" - enable_coverage :branch - primary_coverage :branch - enable_coverage_for_eval - - add_filter "/test/" - add_filter "/rakelib/" - - add_group "Parser", %w[lib/net/imap/response_parser.rb - lib/net/imap/response_parser] - add_group "Config", %w[lib/net/imap/config.rb - lib/net/imap/config] - add_group "SASL", %w[lib/net/imap/sasl.rb - lib/net/imap/sasl - lib/net/imap/authenticators.rb] - add_group "StringPrep", %w[lib/net/imap/stringprep.rb - lib/net/imap/stringprep] +if RUBY_ENGINE == "ruby" # C Ruby only + require "simplecov" + + # Cannot use ".simplecov" file: simplecov-json triggers a circular require. + require "simplecov-json" + SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([ + SimpleCov::Formatter::HTMLFormatter, + SimpleCov::Formatter::JSONFormatter, + ]) + + SimpleCov.start do + command_name "Net::IMAP tests" + enable_coverage :branch + primary_coverage :branch + enable_coverage_for_eval + + add_filter "/test/" + add_filter "/rakelib/" + + add_group "Parser", %w[lib/net/imap/response_parser.rb + lib/net/imap/response_parser] + add_group "Config", %w[lib/net/imap/config.rb + lib/net/imap/config] + add_group "SASL", %w[lib/net/imap/sasl.rb + lib/net/imap/sasl + lib/net/imap/authenticators.rb] + add_group "StringPrep", %w[lib/net/imap/stringprep.rb + lib/net/imap/stringprep] + end end + require "test/unit" require "core_assertions" @@ -54,4 +57,44 @@ def assert_pattern end end + def pend_if(condition, *args, &block) + if condition + pend(*args, &block) + else + block.call if block + end + end + + def pend_unless(condition, *args, &block) + if condition + block.call if block + else + pend(*args, &block) + end + end + + def omit_unless_cruby(msg = "test omitted for non-CRuby", &block) + omit_unless(RUBY_ENGINE == "ruby", msg, &block) + end + + def omit_if_truffleruby(msg = "test omitted on TruffleRuby", &block) + omit_if(RUBY_ENGINE == "truffleruby", msg, &block) + end + + def omit_if_jruby(msg = "test omitted on JRuby", &block) + omit_if(RUBY_ENGINE == "jruby", msg, &block) + end + + def pend_unless_cruby(msg = "test is pending for non-CRuby", &block) + pend_unless(RUBY_ENGINE == "ruby", msg, &block) + end + + def pend_if_truffleruby(msg = "test is pending on TruffleRuby", &block) + pend_if(RUBY_ENGINE == "truffleruby", msg, &block) + end + + def pend_if_jruby(msg = "test is pending on JRuby", &block) + pend_if(RUBY_ENGINE == "jruby", msg, &block) + end + end diff --git a/test/net/imap/test_config.rb b/test/net/imap/test_config.rb index aa9ed5e7..20f1f3f9 100644 --- a/test/net/imap/test_config.rb +++ b/test/net/imap/test_config.rb @@ -270,10 +270,15 @@ def duck.to_r = 1/11111 test "#freeze" do config = Config.new(open_timeout: 1) config.freeze - assert_raise FrozenError do - config.open_timeout = 2 + assert config.frozen? + assert config.__send__(:data).frozen? + pend_if_truffleruby "https://github.com/oracle/truffleruby/issues/3850" do + assert_raise FrozenError do + config.open_timeout = 2 + assert_equal 1, config.open_timeout + end + assert_equal 1, config.open_timeout end - assert_same 1, config.open_timeout end test "#dup" do @@ -302,10 +307,14 @@ def duck.to_r = 1/11111 original.freeze copy = original.clone assert copy.frozen? - assert_raise FrozenError do - copy.open_timeout = 2 + assert copy.__send__(:data).frozen? + pend_if_truffleruby "https://github.com/oracle/truffleruby/issues/3850" do + assert_raise FrozenError do + copy.open_timeout = 2 + assert_equal 1, copy.open_timeout + end + assert_equal 1, copy.open_timeout end - assert_equal 1, copy.open_timeout end test "#inherited? and #reset(attr)" do diff --git a/test/net/imap/test_data_lite.rb b/test/net/imap/test_data_lite.rb index 54c57b89..6ad724ba 100644 --- a/test/net/imap/test_data_lite.rb +++ b/test/net/imap/test_data_lite.rb @@ -154,6 +154,14 @@ def test_inspect end def test_recursive_inspect + if Data.superclass == ::Object + omit_if_truffleruby "TruffleRuby: format('%p', nil) returns '': " \ + "https://github.com/oracle/truffleruby/issues/3846" + else + omit_if_truffleruby "TruffleRuby: Data#inspect has stack overflow: " \ + "https://github.com/oracle/truffleruby/issues/3847" + end + klass = Data.define(:value, :head, :tail) do def initialize(value:, head: nil, tail: nil) case tail @@ -183,6 +191,7 @@ def initialize(value:, head: nil, tail: nil) " tail=#>>>", + # TODO: JRuby's Data fails on the next line list.inspect ) @@ -196,6 +205,7 @@ def initialize(value:, head: nil, tail: nil) " tail=#>>>", + # TODO: JRuby's Data fails on the next line list.inspect ) ensure @@ -340,6 +350,47 @@ def other assert_equal("test", data.name) assert_equal("other", data.other) end + + class Abstract < Data + end + + class Inherited < Abstract.define(:foo) + end + + def test_subclass_can_create + # TODO: JRuby's Data fails all of these + assert_equal 1, Inherited[1] .foo + assert_equal 2, Inherited[foo: 2].foo + assert_equal 3, Inherited.new(3).foo + assert_equal 4, Inherited.new(foo: 4).foo + end + + class AbstractWithClassMethod < Data + def self.inherited_class_method; :ok end + end + + class InheritsClassMethod < AbstractWithClassMethod.define(:foo) + end + + def test_subclass_class_method + # TODO: JRuby's Data fails on the next line + assert_equal :ok, InheritsClassMethod.inherited_class_method + end + + class AbstractWithOverride < Data + def deconstruct; [:ok, *super] end + end + + class InheritsOverride < AbstractWithOverride.define(:foo) + end + + def test_subclass_override_deconstruct + # TODO: JRuby's Data fails on the next line + data = InheritsOverride[:foo] + # TODO: TruffleRuby's Data fails on the next line + assert_equal %i[ok foo], data.deconstruct + end + end end end diff --git a/test/net/imap/test_deprecated_client_options.rb b/test/net/imap/test_deprecated_client_options.rb index ceb290dd..70999a5b 100644 --- a/test/net/imap/test_deprecated_client_options.rb +++ b/test/net/imap/test_deprecated_client_options.rb @@ -55,6 +55,7 @@ class InitializeTests < DeprecatedClientOptionsTest end test "Convert deprecated usessl (= true) and certs, with warning" do + omit_if_jruby "SSL tests don't work yet" run_fake_server_in_thread(implicit_tls: true) do |server| certs = server.config.tls[:ca_file] assert_deprecated_warning(/Call Net::IMAP\.new with keyword/i) do @@ -71,6 +72,7 @@ class InitializeTests < DeprecatedClientOptionsTest end test "Convert deprecated usessl (= true) and verify (= false), with warning" do + omit_if_jruby "SSL tests don't work yet" run_fake_server_in_thread(implicit_tls: true) do |server| assert_deprecated_warning(/Call Net::IMAP\.new with keyword/i) do with_client("localhost", server.port, true, nil, false) do |client| @@ -102,6 +104,7 @@ class InitializeTests < DeprecatedClientOptionsTest class StartTLSTests < DeprecatedClientOptionsTest test "Convert obsolete options hash to keywords" do + omit_if_jruby "SSL tests don't work yet" with_fake_server(preauth: false) do |server, imap| imap.starttls(ca_file: server.config.tls[:ca_file], min_version: :TLS1_2) assert_equal( @@ -114,6 +117,7 @@ class StartTLSTests < DeprecatedClientOptionsTest end test "Convert deprecated certs, verify with warning" do + omit_if_jruby "SSL tests don't work yet" with_fake_server(preauth: false) do |server, imap| assert_deprecated_warning(/Call Net::IMAP#starttls with keyword/i) do imap.starttls(server.config.tls[:ca_file], false) diff --git a/test/net/imap/test_fetch_data.rb b/test/net/imap/test_fetch_data.rb index bcbbf50c..e51484bc 100644 --- a/test/net/imap/test_fetch_data.rb +++ b/test/net/imap/test_fetch_data.rb @@ -14,8 +14,10 @@ def fetch_data_class end test "#uid" do - data = Net::IMAP::FetchData.new(22222, "UID" => 54_321) - assert_equal 54_321, data.uid + pend_if_truffleruby do + data = Net::IMAP::FetchData.new(22222, "UID" => 54_321) + assert_equal 54_321, data.uid + end end end @@ -25,20 +27,22 @@ def fetch_data_class end test "#seqno does not exist" do - data = Net::IMAP::UIDFetchData.new(22222) + data = pend_if_jruby { Net::IMAP::UIDFetchData.new(22222) } or next assert_raise NoMethodError do data.seqno end end test "#uid replaces #seqno" do - data = Net::IMAP::UIDFetchData.new(22222) + data = pend_if_jruby { Net::IMAP::UIDFetchData.new(22222) } or next assert_equal 22222, data.uid end test "#initialize warns when uid differs from attr['UID']" do - assert_warn(/UIDs do not match/i) do - Net::IMAP::UIDFetchData.new(22222, "UID" => 54_321) + pend_if_truffleruby do + assert_warn(/UIDs do not match/i) do + Net::IMAP::UIDFetchData.new(22222, "UID" => 54_321) + end end end diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index dd955b8d..f7037f95 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -28,6 +28,7 @@ def teardown if defined?(OpenSSL::SSL::SSLError) def test_imaps_unknown_ca + omit_if_jruby "SSL tests don't work yet" assert_raise(OpenSSL::SSL::SSLError) do imaps_test do |port| begin @@ -42,6 +43,7 @@ def test_imaps_unknown_ca end def test_imaps_with_ca_file + omit_if_jruby "SSL tests don't work yet" # Assert verified *after* the imaps_test and assert_nothing_raised blocks. # Otherwise, failures can't logout and need to wait for the timeout. verified, imap = :unknown, nil @@ -69,6 +71,7 @@ def test_imaps_with_ca_file end def test_imaps_verify_none + omit_if_jruby "SSL tests don't work yet" # Assert verified *after* the imaps_test and assert_nothing_raised blocks. # Otherwise, failures can't logout and need to wait for the timeout. verified, imap = :unknown, nil @@ -96,6 +99,7 @@ def test_imaps_verify_none end def test_imaps_post_connection_check + omit_if_jruby "SSL tests don't work yet" assert_raise(OpenSSL::SSL::SSLError) do imaps_test do |port| # server_addr is different from the hostname in the certificate, @@ -110,6 +114,7 @@ def test_imaps_post_connection_check if defined?(OpenSSL::SSL) def test_starttls_unknown_ca + omit_if_jruby "SSL tests don't work yet" omit "This test is not working with Windows" if RUBY_PLATFORM =~ /mswin|mingw/ imap = nil @@ -130,6 +135,7 @@ def test_starttls_unknown_ca end def test_starttls + omit_if_jruby "SSL tests don't work yet" initial_verified, initial_ctx, initial_params = :unknown, :unknown, :unknown imap = nil starttls_test do |port| @@ -154,6 +160,7 @@ def test_starttls end def test_starttls_stripping + omit_if_jruby "SSL tests don't work yet" imap = nil starttls_stripping_test do |port| imap = Net::IMAP.new("localhost", :port => port) @@ -512,6 +519,7 @@ def test_connection_closed_during_idle end def test_connection_closed_without_greeting + omit_if_jruby "???" server = create_tcp_server port = server.addr[1] h = { diff --git a/test/net/imap/test_imap_capabilities.rb b/test/net/imap/test_imap_capabilities.rb index ca83a20f..16ad1922 100644 --- a/test/net/imap/test_imap_capabilities.rb +++ b/test/net/imap/test_imap_capabilities.rb @@ -139,6 +139,7 @@ def teardown if defined?(OpenSSL::SSL::SSLError) test "#capabilities caches greeting capabilities (implicit TLS)" do + omit_if_jruby with_fake_server(preauth: false, implicit_tls: true) do |server, imap| assert imap.capabilities_cached? assert_equal %w[IMAP4REV1 AUTH=PLAIN], imap.capabilities @@ -151,6 +152,7 @@ def teardown test "#capabilities cache is cleared after #starttls" do with_fake_server(preauth: false, cleartext_auth: false) do |server, imap| + omit_if_jruby assert imap.capabilities_cached? assert imap.capable? :IMAP4rev1 refute imap.auth_capable? "plain" @@ -204,6 +206,7 @@ def teardown # TODO: should we warn about this? test "#capabilities cache IGNORES tagged OK response to STARTTLS" do + omit_if_jruby with_fake_server(preauth: false) do |server, imap| server.on "STARTTLS" do |cmd| cmd.done_ok code: "[CAPABILITY IMAP4rev1 AUTH=PLAIN fnord]" diff --git a/test/net/imap/test_imap_connection_state.rb b/test/net/imap/test_imap_connection_state.rb index 41f4f5bf..bb990eb4 100644 --- a/test/net/imap/test_imap_connection_state.rb +++ b/test/net/imap/test_imap_connection_state.rb @@ -26,6 +26,7 @@ class ConnectionStateTest < Test::Unit::TestCase end test "#deconstruct" do + # TODO: TruffleRuby's Data fails these assert_equal [:not_authenticated], NotAuthenticated[].deconstruct assert_equal [:authenticated], Authenticated[] .deconstruct assert_equal [:selected], Selected[] .deconstruct @@ -33,6 +34,7 @@ class ConnectionStateTest < Test::Unit::TestCase end test "#deconstruct_keys" do + # TODO: TruffleRuby's Data fails these assert_equal({symbol: :not_authenticated}, NotAuthenticated[].deconstruct_keys([:symbol])) assert_equal({symbol: :authenticated}, Authenticated[] .deconstruct_keys([:symbol])) assert_equal({symbol: :selected}, Selected[] .deconstruct_keys([:symbol])) diff --git a/test/net/imap/test_response_reader.rb b/test/net/imap/test_response_reader.rb index cec6768e..77f506f6 100644 --- a/test/net/imap/test_response_reader.rb +++ b/test/net/imap/test_response_reader.rb @@ -79,9 +79,11 @@ def literal(str) = "{#{str.bytesize}}\r\n#{str}" client.config.max_response_size = 10 io = StringIO.new(barely_over) rcvr = Net::IMAP::ResponseReader.new(client, io) - assert_raise Net::IMAP::ResponseTooLargeError do - result = rcvr.read_response_buffer - flunk "Got result: %p" % [result] + pend_if_truffleruby do + assert_raise Net::IMAP::ResponseTooLargeError do + result = rcvr.read_response_buffer + flunk "Got result: %p" % [result] + end end end