From 739ba7bd1c1ac5598256d0c7ec13005b74e9b0b3 Mon Sep 17 00:00:00 2001 From: nick evans Date: Wed, 7 May 2025 16:38:05 -0400 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=85=F0=9F=9A=A7=20Omit=20many=20SSL?= =?UTF-8?q?=20connection=20tests=20for=20JRuby?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See also: * jruby/jruby#9529 --- test/net/imap/test_imap.rb | 9 +++++++++ test/net/imap/test_imap_capabilities.rb | 3 +++ test/net/imap/test_imap_deprecated_client_options.rb | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/test/net/imap/test_imap.rb b/test/net/imap/test_imap.rb index d005bc502..0110ed664 100644 --- a/test/net/imap/test_imap.rb +++ b/test/net/imap/test_imap.rb @@ -13,6 +13,7 @@ class IMAPTest < Net::IMAP::TestCase if defined?(OpenSSL::SSL::SSLError) def test_imaps_unknown_ca + omit_if_jruby "SSL tests don't work yet" assert_local_raise(OpenSSL::SSL::SSLError) do imaps_test do |port| begin @@ -27,6 +28,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 @@ -54,6 +56,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 @@ -81,6 +84,7 @@ def test_imaps_verify_none end def test_imaps_post_connection_check + omit_if_jruby "SSL tests don't work yet" assert_local_raise(OpenSSL::SSL::SSLError) do imaps_test do |port| # server_addr is different from the hostname in the certificate, @@ -95,6 +99,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 @@ -116,6 +121,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| @@ -141,6 +147,7 @@ def test_starttls end def test_starttls_stripping_not_ok + omit_if_jruby "SSL tests don't work yet" imap = nil server = create_tcp_server port = server.addr[1] @@ -173,6 +180,7 @@ def test_starttls_stripping_not_ok end def test_starttls_stripping_ok_sent_before_response + omit_if_jruby "SSL tests don't work yet" # to coordinate between threads (better than sleep) server_to_client, client_to_server = Queue.new, Queue.new rcvr_to_client = Queue.new @@ -677,6 +685,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 c2274798c..9d605d57c 100644 --- a/test/net/imap/test_imap_capabilities.rb +++ b/test/net/imap/test_imap_capabilities.rb @@ -124,6 +124,7 @@ class IMAPCapabilitiesTest < Net::IMAP::TestCase 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 @@ -136,6 +137,7 @@ class IMAPCapabilitiesTest < Net::IMAP::TestCase 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" @@ -189,6 +191,7 @@ class IMAPCapabilitiesTest < Net::IMAP::TestCase # 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_deprecated_client_options.rb b/test/net/imap/test_imap_deprecated_client_options.rb index 9ff9ad23e..b02b49bdc 100644 --- a/test/net/imap/test_imap_deprecated_client_options.rb +++ b/test/net/imap/test_imap_deprecated_client_options.rb @@ -42,6 +42,7 @@ class InitializeTests < IMAPDeprecatedClientOptionsTest 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 @@ -58,6 +59,7 @@ class InitializeTests < IMAPDeprecatedClientOptionsTest 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| @@ -89,6 +91,7 @@ class InitializeTests < IMAPDeprecatedClientOptionsTest class StartTLSTests < IMAPDeprecatedClientOptionsTest 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( @@ -101,6 +104,7 @@ class StartTLSTests < IMAPDeprecatedClientOptionsTest 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) From 883176127d735048280af47e28fcb141e2b4004c Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 19 Jul 2026 12:34:27 -0400 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9C=85=F0=9F=9A=A7=20Mark=20local=20stac?= =?UTF-8?q?ktrace=20tests=20pending=20for=20JRuby?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stacktraces just don't line up. See also: * jruby/jruby#9528 --- test/lib/helper.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/lib/helper.rb b/test/lib/helper.rb index 0977b2e8e..a45ed8c75 100644 --- a/test/lib/helper.rb +++ b/test/lib/helper.rb @@ -228,7 +228,9 @@ def assert_local_raise(expected, message = nil) assert_raise(expected, &block) end stack = caller - assert_equal stack, error.backtrace&.last(stack.size) + pend_if_jruby("stack traces don't match in JRuby 10.0.1.0") do + assert_equal stack, error.backtrace&.last(stack.size) + end error end From a4ca7cbae7c03998c4b336d241fd36efa5d060d3 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 19 Jul 2026 12:16:05 -0400 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9C=85=F0=9F=9A=A7=20Omit=20ResponsePars?= =?UTF-8?q?eError=20test=20for=20JRuby?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/net/imap/test_errors.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/net/imap/test_errors.rb b/test/net/imap/test_errors.rb index 6a07a7fbc..433cc75db 100644 --- a/test/net/imap/test_errors.rb +++ b/test/net/imap/test_errors.rb @@ -107,6 +107,7 @@ def self.SGR(*attr) = CSI attr.join(?;), ?m ENV["TERM"], ENV["NO_COLOR"], ENV["FORCE_COLOR"] = nil, nil, "0" assert_equal(expected_no_hl, err.detailed_message) + omit_if_jruby "JRuby doesn't support fully format string hash lookup" assert_equal(expected_color_hl, err.detailed_message(highlight: true)) assert_equal(expected_no_color, err.detailed_message(highlight: true, highlight_no_color: true)) From 116956d2ab800da4526db6c4b6b05b1f55e84cc8 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 19 Jul 2026 12:31:54 -0400 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=A9=B9=20Workaround=20JRuby=20bug=20N?= =?UTF-8?q?oMatchingPatternError#=3D=3D=3D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For some unknown reason, JRuby's `NoMatchingPatternKeyError#===` doesn't match `NoMatchingPatternKeyError` objects. See also: * jruby/jruby/#9537 --- lib/net/imap/errors.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/imap/errors.rb b/lib/net/imap/errors.rb index a042604e6..0475e10de 100644 --- a/lib/net/imap/errors.rb +++ b/lib/net/imap/errors.rb @@ -340,7 +340,7 @@ def initialize(state, response:, command: nil) @state, @command, @response = state, command, response cmd_desc = name ? "#{state} #{name}" : state super "Received tagged #{status} to #{cmd_desc} command (tag=#{tag})" - rescue NoMatchingPatternError => err + rescue NoMatchingPatternError, NoMatchingPatternKeyError => err raise ArgumentError, err.message end From 8890721db0468584b51da8d3062f5e8c5a246835 Mon Sep 17 00:00:00 2001 From: nick evans Date: Thu, 12 Feb 2026 10:09:59 -0500 Subject: [PATCH 5/8] =?UTF-8?q?=E2=8F=AA=F0=9F=9A=A7=20Bring=20back=20Data?= =?UTF-8?q?=20polyfill=20for=20JRuby,=20TruffleRuby?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Data polyfill was removed in v0.6.0, but I'm bringing it back so we can work around JRuby and TruffleRuby test failures. The polyfill is _only_ loaded when RUBY_ENGINE is either jruby or truffleruby. In those ruby engines, several compatibility tests are run, and the polyfill is only laoded when one of those tests fail. Some bugs related to this (which may have been fixed?): * jruby/jruby#8829 * jruby/jruby#9530 * oracle/truffleruby#3846 * oracle/truffleruby#3847 fixup --- lib/net/imap.rb | 3 + lib/net/imap/data_polyfill.rb | 244 ++++++++++++++++++ test/net/imap/test_data_polyfill.rb | 384 ++++++++++++++++++++++++++++ 3 files changed, 631 insertions(+) create mode 100644 lib/net/imap/data_polyfill.rb create mode 100644 test/net/imap/test_data_polyfill.rb diff --git a/lib/net/imap.rb b/lib/net/imap.rb index e9fbb3ef7..20d4b30e3 100644 --- a/lib/net/imap.rb +++ b/lib/net/imap.rb @@ -4072,6 +4072,9 @@ def self.saslprep(string, **opts) end end +# TODO: remove after TruffleRuby and JRuby bugs are fixed +require_relative "imap/data_polyfill" + require_relative "imap/errors" require_relative "imap/config" require_relative "imap/command_data" diff --git a/lib/net/imap/data_polyfill.rb b/lib/net/imap/data_polyfill.rb new file mode 100644 index 000000000..bc8e49727 --- /dev/null +++ b/lib/net/imap/data_polyfill.rb @@ -0,0 +1,244 @@ +# frozen_string_literal: true + +# Some of the code in this file was copied from the polyfill-data gem. +# +# MIT License +# +# Copyright (c) 2023 Jim Gay, Joel Drapper, Nicholas Evans +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# simplecov:disable -- Skip test coverage for this file + +unless RUBY_ENGINE in "jruby" | "truffleruby" + Net::IMAP::Data = ::Data + return # don't load the rest of the file +end +begin + module TestData + Incompatible = Class.new(StandardError) + + class Abstract < ::Data + def self.class_method = :ok + def deconstruct = [:ok, *super] + end + + class Inherits < Abstract.define(:foo) + end + + errors = [] + begin + data = Inherits.new(1) + data.foo.nil? and errors << "member attribute" + data.deconstruct in :ok, 1 or errors << "inherited #deconstruct" + Inherits.class_method rescue errors << "inherited class method" + rescue => error + errors << error.detailed_message(highlight: false, did_you_mean: false) + end + raise Incompatible, errors.join(", ") if errors.any? + end +rescue TestData::Incompatible => err + warn "Detected incompatible Data implementation: #{err}." +else + Net::IMAP::Data = ::Data + return # don't load the rest of the file +ensure + Object.module_exec do remove_const :TestData end if defined?(TestData) +end + +warn "Using Net::IMAP::Data polyfill." + +module Net + class IMAP + # DataPolyfill is a temporary substitute for ruby 3.2's +Data+ class, but it + # is only loaded by JRuby and TruffleRuby. The core +Data+ class is + # tested, and when it is sufficiently compatible with CRuby, DataPolyfill + # won't be loaded. + # + # DataPolyfill is aliased as Net::IMAP::Data, so that Net::IMAP code can use + # it as +Data+. + # + # See {ruby 3.2's documentation for Data}[https://docs.ruby-lang.org/en/3.2/Data.html]. + # + # Some of the code in this class was copied or adapted from the + # {polyfill-data gem}[https://rubygems.org/gems/polyfill-data], by Jim Gay + # and Joel Drapper, under the MIT license terms. + class DataPolyfill + singleton_class.undef_method :new + + TYPE_ERROR = "%p is not a symbol nor a string" + ATTRSET_ERROR = "invalid data member: %p" + DUP_ERROR = "duplicate member: %p" + ARITY_ERROR = "wrong number of arguments (given %d, expected %s)" + private_constant :TYPE_ERROR, :ATTRSET_ERROR, :DUP_ERROR, :ARITY_ERROR + + # Defines a new Data class. + # + # _NOTE:_ Unlike ruby 3.2's +Data.define+, DataPolyfill.define only + # supports member names which are valid local variable names. Member + # names can't be keywords (e.g: +next+ or +class+) or start with capital + # letters, "@", etc. + def self.define(*args, &block) + members = args.each_with_object({}) do |arg, members| + arg = arg.to_str unless arg in Symbol | String if arg.respond_to?(:to_str) + arg = arg.to_sym if arg in String + arg in Symbol or raise TypeError, TYPE_ERROR % [arg] + arg in %r{=} and raise ArgumentError, ATTRSET_ERROR % [arg] + members.key?(arg) and raise ArgumentError, DUP_ERROR % [arg] + members[arg] = true + end + members = members.keys.freeze + + klass = ::Class.new(self) + + klass.singleton_class.undef_method :define + klass.define_singleton_method(:members) { members } + + def klass.new(*args, **kwargs, &block) + if kwargs.size.positive? + if args.size.positive? + raise ArgumentError, ARITY_ERROR % [args.size, 0] + end + elsif members.size < args.size + expected = members.size.zero? ? 0 : 0..members.size + raise ArgumentError, ARITY_ERROR % [args.size, expected] + else + kwargs = Hash[members.take(args.size).zip(args)] + end + allocate.tap do |instance| + instance.__send__(:initialize, **kwargs, &block) + end.freeze + end + + klass.singleton_class.alias_method :[], :new + klass.attr_reader(*members) + + # Dynamically defined initializer methods are in an included module, + # rather than directly on DataPolyfill (like in ruby 3.2+): + # * simpler to handle required kwarg ArgumentErrors + # * easier to ensure consistent ivar assignment order (object shape) + # * faster than instance_variable_set + klass.include(Module.new do + if members.any? + kwargs = members.map{"#{_1.name}:"}.join(", ") + params = members.map(&:name).join(", ") + ivars = members.map{"@#{_1.name}"}.join(", ") + attrs = members.map{"attrs[:#{_1.name}]"}.join(", ") + module_eval <<~RUBY, __FILE__, __LINE__ + 1 + protected + def initialize(#{kwargs}) #{ivars} = #{params}; freeze end + def marshal_load(attrs) #{ivars} = #{attrs}; freeze end + RUBY + end + end) + + klass.module_eval do _1.module_eval(&block) end if block_given? + + klass + end + + ## + # singleton-method: new + # call-seq: + # new(*args) -> instance + # new(**kwargs) -> instance + # + # Constuctor for classes defined with ::define. + # + # Aliased as ::[]. + + ## + # singleton-method: [] + # call-seq: + # ::[](*args) -> instance + # ::[](**kwargs) -> instance + # + # Constuctor for classes defined with ::define. + # + # Alias for ::new + + ## + def members; self.class.members end + def to_h(&block) block ? __to_h__.to_h(&block) : __to_h__ end + def hash; [self.class, __to_h__].hash end + def ==(other) self.class == other.class && to_h == other.to_h end + def eql?(other) self.class == other.class && hash == other.hash end + def deconstruct; __to_h__.values end + + def deconstruct_keys(keys) + raise TypeError unless keys.is_a?(Array) || keys.nil? + return __to_h__ if keys&.first.nil? + __to_h__.slice(*keys) + end + + def with(**kwargs) + return self if kwargs.empty? + self.class.new(**__to_h__.merge(kwargs)) + end + + def inspect + __inspect_guard__(self) do |seen| + return "#" if seen + attrs = __to_h__.map {|kv| "%s=%p" % kv }.join(", ") + display = ["data", self.class.name, attrs].compact.join(" ") + "#<#{display}>" + end + end + alias_method :to_s, :inspect + + private + + def initialize_copy(source) super.freeze end + def marshal_dump; __to_h__ end + + def __to_h__; Hash[members.map {|m| [m, send(m)] }] end + + # Yields +true+ if +obj+ has been seen already, +false+ if it hasn't. + # Marks +obj+ as seen inside the block, so circuler references don't + # recursively trigger a SystemStackError (stack level too deep). + # + # Making circular references inside a Data object _should_ be very + # uncommon, but we'll support them for the sake of completeness. + def __inspect_guard__(obj) + preexisting = Thread.current[:__net_imap_data__inspect__] + Thread.current[:__net_imap_data__inspect__] ||= {}.compare_by_identity + inspect_guard = Thread.current[:__net_imap_data__inspect__] + if inspect_guard.include?(obj) + yield true + else + begin + inspect_guard[obj] = true + yield false + ensure + inspect_guard.delete(obj) + end + end + ensure + unless preexisting.equal?(inspect_guard) + Thread.current[:__net_imap_data__inspect__] = preexisting + end + end + + end + + Data = DataPolyfill + end +end + +# simplecov:enable diff --git a/test/net/imap/test_data_polyfill.rb b/test/net/imap/test_data_polyfill.rb new file mode 100644 index 000000000..d2a0f5855 --- /dev/null +++ b/test/net/imap/test_data_polyfill.rb @@ -0,0 +1,384 @@ +# frozen_string_literal: false + +require "net/imap" +require "test/unit" + +# This test file was copied from the polyfill-data gem. +# +# MIT License +# +# Copyright (c) 2023 Jim Gay +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +module Net + class IMAP + class TestData < Net::IMAP::TestCase + Data = Net::IMAP::Data + + def test_define + klass = Data.define(:foo, :bar) + assert_kind_of(Class, klass) + assert_equal(%i[foo bar], klass.members) + + assert_raise(NoMethodError) { Data.new(:foo) } + assert_raise(TypeError) { Data.define(0) } + + # Because some code is shared with Struct, check we don't share unnecessary functionality + assert_raise(TypeError) { Data.define(:foo, keyword_init: true) } + + refute_respond_to(Data.define, :define, "Cannot define from defined Data class") + end + + def test_define_edge_cases + # non-ascii + klass = Data.define(:"r\u{e9}sum\u{e9}") + o = klass.new(1) + assert_equal(1, o.send(:"r\u{e9}sum\u{e9}")) + + assert_raise(ArgumentError) { Data.define(:x=) } + assert_raise(ArgumentError, /duplicate member/) { Data.define(:x, :x) } + end + + def test_define_with_block + klass = Data.define(:a, :b) do + def c + a + b + end + end + + assert_equal(3, klass.new(1, 2).c) + end + + def test_initialize + klass = Data.define(:foo, :bar) + + # Regular + test = klass.new(1, 2) + assert_equal(1, test.foo) + assert_equal(2, test.bar) + assert_equal(test, klass.new(1, 2)) + assert_predicate(test, :frozen?) + + # Keywords + test_kw = klass.new(foo: 1, bar: 2) + assert_equal(1, test_kw.foo) + assert_equal(2, test_kw.bar) + assert_equal(test_kw, klass.new(foo: 1, bar: 2)) + assert_equal(test_kw, test) + + # Wrong protocol + assert_raise(ArgumentError) { klass.new(1) } + assert_raise(ArgumentError) { klass.new(1, 2, 3) } + assert_raise(ArgumentError) { klass.new(foo: 1) } + assert_raise(ArgumentError) { klass.new(foo: 1, bar: 2, baz: 3) } + # Could be converted to foo: 1, bar: 2, but too smart is confusing + assert_raise(ArgumentError) { klass.new(1, bar: 2) } + end + + def test_initialize_redefine + klass = Data.define(:foo, :bar) do + attr_reader :passed + + def initialize(*args, **kwargs) + @passed = [args, kwargs] + + super(foo: 1, bar: 2) # so we can experiment with passing wrong numbers of args + end + end + + assert_equal([[], {foo: 1, bar: 2}], klass.new(foo: 1, bar: 2).passed) + + # Positional arguments are converted to keyword ones + assert_equal([[], {foo: 1, bar: 2}], klass.new(1, 2).passed) + + # Missing arguments can be fixed in initialize + assert_equal([[], {foo: 1}], klass.new(foo: 1).passed) + + # Extra keyword arguments can be dropped in initialize + assert_equal([[], {foo: 1, bar: 2, baz: 3}], klass.new(foo: 1, bar: 2, baz: 3).passed) + end + + def test_instance_behavior + klass = Data.define(:foo, :bar) + + test = klass.new(1, 2) + assert_equal(1, test.foo) + assert_equal(2, test.bar) + assert_equal(%i[foo bar], test.members) + assert_equal(1, test.public_send(:foo)) + assert_equal(0, test.method(:foo).arity) + assert_equal([], test.method(:foo).parameters) + + assert_equal({foo: 1, bar: 2}, test.to_h) + assert_equal({"foo"=>"1", "bar"=>"2"}, test.to_h { [_1.to_s, _2.to_s] }) + + assert_equal({foo: 1, bar: 2}, test.deconstruct_keys(nil)) + assert_equal({foo: 1}, test.deconstruct_keys(%i[foo])) + assert_equal({foo: 1}, test.deconstruct_keys(%i[foo baz])) + assert_raise(TypeError) { test.deconstruct_keys(0) } + + test = klass.new(bar: 2, foo: 1) + assert_equal([1, 2], test.deconstruct) + + assert_kind_of(Integer, test.hash) + end + + def test_inspect + klass = Data.define(:a) + o = klass.new(1) + assert_equal("#", o.inspect) + + Object.const_set(:Foo, klass) + assert_equal("#", o.inspect) + Object.instance_eval { remove_const(:Foo) } + + klass = Data.define(:one, :two) + o = klass.new(1,2) + assert_equal("#", o.inspect) + assert_equal("#", o.to_s) + end + + def test_recursive_inspect + klass = Data.define(:value, :head, :tail) do + def initialize(value:, head: nil, tail: nil) + case tail + in Array if tail.empty? + tail = nil + in Array + succ, *rest = *tail + tail = self.class[head: self, value: succ, tail: rest] + in [tailprev, _, _] if tail.class == self.class && tailprev == self + # noop + in [tailprev, succ, rest] if tail.class == self.class + tail = self.class[head: self, value: succ, tail: rest] + in nil + else + tail = self.class[head: self, value: tail, tail: nil] + end + super(head:, value:, tail:) + end + end + + # anonymous class + list = klass[value: 1, tail: [2, 3, 4]] + seen = "#" + assert_equal( + "#>>>", + list.inspect + ) + + # named class + Object.const_set(:DoubleLinkList, klass) + list = DoubleLinkList[value: 1, tail: [2, 3, 4]] + seen = "#" + assert_equal( + "#>>>", + list.inspect + ) + ensure + Object.instance_eval { remove_const(:DoubleLinkList) } rescue nil + end + + def test_equal + klass1 = Data.define(:a) + klass2 = Data.define(:a) + o1 = klass1.new(1) + o2 = klass1.new(1) + o3 = klass2.new(1) + assert_equal(o1, o2) + refute_equal(o1, o3) + end + + def test_eql + klass1 = Data.define(:a) + klass2 = Data.define(:a) + o1 = klass1.new(1) + o2 = klass1.new(1) + o3 = klass2.new(1) + assert_operator(o1, :eql?, o2) + refute_operator(o1, :eql?, o3) + end + + def test_with + klass = Data.define(:foo, :bar) + source = klass.new(foo: 1, bar: 2) + + # Simple + test = source.with + assert_equal(source.object_id, test.object_id) + + # Changes + test = source.with(foo: 10) + + assert_equal(1, source.foo) + assert_equal(2, source.bar) + assert_equal(source, klass.new(foo: 1, bar: 2)) + + assert_equal(10, test.foo) + assert_equal(2, test.bar) + assert_equal(test, klass.new(foo: 10, bar: 2)) + + test = source.with(foo: 10, bar: 20) + + assert_equal(1, source.foo) + assert_equal(2, source.bar) + assert_equal(source, klass.new(foo: 1, bar: 2)) + + assert_equal(10, test.foo) + assert_equal(20, test.bar) + assert_equal(test, klass.new(foo: 10, bar: 20)) + + # Keyword splat + changes = { foo: 10, bar: 20 } + test = source.with(**changes) + + assert_equal(1, source.foo) + assert_equal(2, source.bar) + assert_equal(source, klass.new(foo: 1, bar: 2)) + + assert_equal(10, test.foo) + assert_equal(20, test.bar) + assert_equal(test, klass.new(foo: 10, bar: 20)) + + # Wrong protocol + assert_raise(ArgumentError, "wrong number of arguments (given 1, expected 0)") do + source.with(10) + end + assert_raise(ArgumentError, "unknown keywords: :baz, :quux") do + source.with(foo: 1, bar: 2, baz: 3, quux: 4) + end + assert_raise(ArgumentError, "wrong number of arguments (given 1, expected 0)") do + source.with(1, bar: 2) + end + assert_raise(ArgumentError, "wrong number of arguments (given 2, expected 0)") do + source.with(1, 2) + end + assert_raise(ArgumentError, "wrong number of arguments (given 1, expected 0)") do + source.with({ bar: 2 }) + end unless RUBY_VERSION < "2.8.0" + end + + def test_memberless + klass = Data.define + + test = klass.new + + assert_equal(klass.new, test) + refute_equal(Data.define.new, test) + + assert_match(/#/, test.inspect) + assert_equal([], test.members) + assert_equal({}, test.to_h) + end + + def test_square_braces + klass = Data.define(:amount, :unit) + + distance = klass[10, 'km'] + + assert_equal(10, distance.amount) + assert_equal('km', distance.unit) + end + + def test_dup + klass = Data.define(:foo, :bar) + test = klass.new(foo: 1, bar: 2) + assert_equal(klass.new(foo: 1, bar: 2), test.dup) + assert_predicate(test.dup, :frozen?) + end + + Klass = Data.define(:foo, :bar) + + def test_marshal + test = Klass.new(foo: 1, bar: 2) + loaded = Marshal.load(Marshal.dump(test)) + assert_equal(test, loaded) + refute_same(test, loaded) + assert_predicate(loaded, :frozen?) + end + + def test_member_precedence + name_mod = Module.new do + def name + "default name" + end + + def other + "other" + end + end + + klass = Data.define(:name) do + include name_mod + end + + data = klass.new("test") + + 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 + 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 + 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 + data = InheritsOverride[:foo] + assert_equal %i[ok foo], data.deconstruct + end + + end + end +end From 5d27e49aa4fc977b1d6610fa2d9801ff9d562dbd Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 19 Jul 2026 12:26:04 -0400 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=85=F0=9F=9A=A7=20Let=20YAML=20fixtur?= =?UTF-8?q?e=20load=20fail=20in=20JRuby/TruffleRuby?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unfortunately, DataPolyfill isn't compatible with YAML loading. --- test/net/imap/net_imap_test_helpers.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/net/imap/net_imap_test_helpers.rb b/test/net/imap/net_imap_test_helpers.rb index d7ecac233..01ca72c51 100644 --- a/test/net/imap/net_imap_test_helpers.rb +++ b/test/net/imap/net_imap_test_helpers.rb @@ -12,6 +12,16 @@ module TestFixtureGenerators def load_fixture_data(*test_fixture_path) dir = self::TEST_FIXTURE_PATH YAML.unsafe_load_file File.join(dir, *test_fixture_path) + rescue TypeError => err + case err.message + in /bind argument must be an instance of Data/ + raise unless defined?(Net::IMAP::DataPolyfill) + file = test_fixture_path.last + warn "⚠️ Can't load test fixture #{file}: #{err.detailed_message}" + {tests: []} + else + raise + end end def generate_tests_from(fixture_data: nil, fixture_file: nil) From 37d871728f751b7409681343202212074ca8109e Mon Sep 17 00:00:00 2001 From: nick evans Date: Mon, 5 May 2025 15:22:03 -0400 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9C=85=F0=9F=9A=A7=20Run=20CI=20with=20J?= =?UTF-8?q?Ruby,=20too?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using jruby-head to get some bugfixes. --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 301906e57..4b1ab78f6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: experimental: [false] include: - { ruby: truffleruby, os: ubuntu-latest, experimental: true } + - { ruby: jruby-head, os: ubuntu-latest, experimental: true } runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} timeout-minutes: 15 From 7507ed078177691ea41cf6840e456dd7f9825631 Mon Sep 17 00:00:00 2001 From: nick evans Date: Sun, 19 Jul 2026 18:16:21 -0400 Subject: [PATCH 8/8] =?UTF-8?q?=E2=9C=85=20Un-pend=20TruffleRuby=20tests?= =?UTF-8?q?=20using=20Data-polyfill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/net/imap/test_connection_state.rb | 28 +++++++++++--------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/test/net/imap/test_connection_state.rb b/test/net/imap/test_connection_state.rb index ec6b11ebd..8309de5d1 100644 --- a/test/net/imap/test_connection_state.rb +++ b/test/net/imap/test_connection_state.rb @@ -25,25 +25,21 @@ class ConnectionStateTest < Net::IMAP::TestCase end test "#deconstruct" do - pend_if_truffleruby "TruffleRuby bug overriding ::Data methods" do - assert_equal [:not_authenticated], NotAuthenticated[].deconstruct - assert_equal [:authenticated], Authenticated[] .deconstruct - assert_equal [:selected], Selected[] .deconstruct - assert_equal [:logout], Logout[] .deconstruct - end + assert_equal [:not_authenticated], NotAuthenticated[].deconstruct + assert_equal [:authenticated], Authenticated[] .deconstruct + assert_equal [:selected], Selected[] .deconstruct + assert_equal [:logout], Logout[] .deconstruct end test "#deconstruct_keys" do - pend_if_truffleruby "TruffleRuby bug overriding ::Data methods" do - 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])) - assert_equal({symbol: :logout}, Logout[] .deconstruct_keys([:symbol])) - assert_equal({name: "not_authenticated"}, NotAuthenticated[].deconstruct_keys([:name])) - assert_equal({name: "authenticated"}, Authenticated[] .deconstruct_keys([:name])) - assert_equal({name: "selected"}, Selected[] .deconstruct_keys([:name])) - assert_equal({name: "logout"}, Logout[] .deconstruct_keys([:name])) - end + 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])) + assert_equal({symbol: :logout}, Logout[] .deconstruct_keys([:symbol])) + assert_equal({name: "not_authenticated"}, NotAuthenticated[].deconstruct_keys([:name])) + assert_equal({name: "authenticated"}, Authenticated[] .deconstruct_keys([:name])) + assert_equal({name: "selected"}, Selected[] .deconstruct_keys([:name])) + assert_equal({name: "logout"}, Logout[] .deconstruct_keys([:name])) end test "#not_authenticated?" do