Skip to content

✅ Add TruffleRuby and JRuby to CI #470

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

Draft
wants to merge 20 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d4b0b8f
🐛 Synchronize state_authenticated!
nevans May 7, 2025
5db59fe
♻️ Always go into logout state in #disconnect
nevans May 9, 2025
e600e7b
♻️ Short-circuit logout state transition
nevans May 9, 2025
286f757
♻️ Don't wait for lock _before_ disconnecting
nevans May 9, 2025
29cd0c5
♻️ Join the receiver _after_ closing the socket
nevans May 9, 2025
e7a6678
♻️ Tweak connection state change in receiver thread
nevans May 9, 2025
400ac04
🐛 Always remove idle response handler after done
nevans May 9, 2025
0ffabf2
🔀 Merge branch 'synchronize-state_authenticated'
nevans May 8, 2025
2d9cbb7
✅ Run CI with JRuby and TruffleRuby, too
nevans May 5, 2025
631a765
✅ Skip simplecov for non-CRuby engines
nevans Apr 28, 2025
ec16703
✅ Check Data.respond_to?(:define) for Data polyfill
nevans Apr 26, 2025
66034dc
🚧 Check if the Data implementation is sufficient
nevans May 6, 2025
8ce8f5a
✅ Add TruffleRuby/JRuby pend/omit test helpers
nevans May 5, 2025
3d5e179
✅🚧 Omit 1 DataLite test for TruffleRuby
nevans May 5, 2025
b37ef44
✅🚧 Mark 1 ResponseReader test as pending for TruffleRuby
nevans May 7, 2025
20f8aca
✅🚧 Mark 2 FetchData tests as pending for JRuby
nevans May 7, 2025
26b8819
✅🚧 Mark 2 FetchData tests as pending for TruffleRuby
nevans May 7, 2025
aa255b2
✅🚧 Mark 2 Config tests as pending for TruffleRuby
nevans May 7, 2025
4d81b3f
✅🚧 Omit 15 SSL connection tests for JRuby
nevans May 7, 2025
e0c2e23
🚧✅ Ruby head is failing—mark as "experimental"
nevans May 13, 2025
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
14 changes: 8 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
30 changes: 24 additions & 6 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
54 changes: 49 additions & 5 deletions lib/net/imap/data_lite.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
97 changes: 70 additions & 27 deletions test/lib/helper.rb
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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
21 changes: 15 additions & 6 deletions test/net/imap/test_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading