Skip to content

Commit 0027548

Browse files
koji-takao-spclaude
andcommitted
Add Ruby 4.0 support with minitest 6.x compatibility
- Update gemspec to allow minitest >= 5.0 (includes 6.x for Ruby 4.0) - Update ci.yml to remove Gemfile.lock before bundle install for fresh dependency resolution per Ruby version - Refactor tests to not use Object#stub (removed in minitest 6.x) - garbage_test.rb: manually create and call finalizer - finalize_freeze_test.rb: expose finalizer via class method Test results: - Ruby 2.6: 20 tests passed - Ruby 3.3: 20 tests passed - Ruby 4.0: 20 tests passed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 893e1a1 commit 0027548

5 files changed

Lines changed: 78 additions & 37 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020

2121
steps:
2222
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
23+
- name: Remove Gemfile.lock for fresh dependency resolution
24+
run: rm -f Gemfile.lock
2325
- name: Set up Ruby
2426
uses: ruby/setup-ruby@4c24fa5ec04b2e79eb40571b1cee2a0d2b705771 # v1.278.0
2527
with:

schmooze.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ Gem::Specification.new do |spec|
1919

2020
spec.required_ruby_version = '>= 2.6.0'
2121
spec.add_development_dependency 'rake'
22-
spec.add_development_dependency 'minitest', '~> 5.0'
22+
spec.add_development_dependency 'minitest', '>= 5.0'
2323
end

test/finalize_freeze_test.rb

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ class LongRunningSchmoozer < Schmooze::Base
99
# This method keeps the Node.js process running with a setTimeout
1010
# Even after stdin is closed, the process will wait for the timeout
1111
method :echo, 'function(x) { setTimeout(() => {}, 60000); return x; }'
12+
13+
# Expose the finalizer for testing
14+
def self.create_finalizer(owner_pid, stdin, stdout, stderr, process_thread)
15+
finalize(owner_pid, stdin, stdout, stderr, process_thread)
16+
end
1217
end
1318

1419
# Test that the finalizer does not hang when the process is still running.
@@ -21,19 +26,23 @@ class LongRunningSchmoozer < Schmooze::Base
2126
# The fix uses `Process.kill(:KILL, pid)` to actually terminate the process
2227
# before waiting for it.
2328
def test_finalizer_does_not_hang
24-
finalizer = nil
25-
pid = nil
29+
# Create a schmoozer and get its internal state
30+
schmoozer = LongRunningSchmoozer.new(__dir__)
31+
schmoozer.echo("test")
32+
pid = schmoozer.pid
2633

27-
# Capture the finalizer without letting it run automatically
28-
ObjectSpace.stub :define_finalizer, proc { |_s, p| finalizer = p } do
29-
schmoozer = LongRunningSchmoozer.new(__dir__)
30-
schmoozer.echo("test")
31-
pid = schmoozer.pid
34+
# Get the internal process data
35+
stdin = schmoozer.instance_variable_get(:@_schmooze_stdin)
36+
stdout = schmoozer.instance_variable_get(:@_schmooze_stdout)
37+
stderr = schmoozer.instance_variable_get(:@_schmooze_stderr)
38+
process_thread = schmoozer.instance_variable_get(:@_schmooze_process_thread)
3239

33-
# Verify the process is running
34-
assert pid, "Process should be running"
35-
Process.kill(0, pid) # Should not raise if process is running
36-
end
40+
# Create a finalizer manually (simulating what ObjectSpace.define_finalizer does)
41+
finalizer = LongRunningSchmoozer.create_finalizer(Process.pid, stdin, stdout, stderr, process_thread)
42+
43+
# Verify the process is running
44+
assert pid, "Process should be running"
45+
Process.kill(0, pid) # Should not raise if process is running
3746

3847
# Run the finalizer with a timeout to detect hanging
3948
assert_raises_nothing_within(5) do
@@ -50,17 +59,24 @@ def test_finalizer_does_not_hang
5059
# This tests the scenario where GC.stress is enabled and many instances
5160
# are created and garbage collected.
5261
def test_finalizer_handles_multiple_instances_under_gc_pressure
53-
pids = []
62+
instances = []
5463
finalizers = []
5564

56-
ObjectSpace.stub :define_finalizer, proc { |_s, p| finalizers << p } do
57-
5.times do
58-
schmoozer = LongRunningSchmoozer.new(__dir__)
59-
schmoozer.echo("test")
60-
pids << schmoozer.pid
61-
end
65+
5.times do
66+
schmoozer = LongRunningSchmoozer.new(__dir__)
67+
schmoozer.echo("test")
68+
69+
stdin = schmoozer.instance_variable_get(:@_schmooze_stdin)
70+
stdout = schmoozer.instance_variable_get(:@_schmooze_stdout)
71+
stderr = schmoozer.instance_variable_get(:@_schmooze_stderr)
72+
process_thread = schmoozer.instance_variable_get(:@_schmooze_process_thread)
73+
74+
instances << { schmoozer: schmoozer, pid: schmoozer.pid }
75+
finalizers << LongRunningSchmoozer.create_finalizer(Process.pid, stdin, stdout, stderr, process_thread)
6276
end
6377

78+
pids = instances.map { |i| i[:pid] }
79+
6480
assert_equal 5, pids.length
6581
assert_equal 5, finalizers.length
6682

@@ -81,14 +97,17 @@ def test_finalizer_handles_multiple_instances_under_gc_pressure
8197
def test_finalizer_is_fork_safe
8298
skip "Fork not available on this platform" unless Process.respond_to?(:fork)
8399

84-
finalizer = nil
85-
pid = nil
100+
schmoozer = LongRunningSchmoozer.new(__dir__)
101+
schmoozer.echo("test")
102+
pid = schmoozer.pid
86103

87-
ObjectSpace.stub :define_finalizer, proc { |_s, p| finalizer = p } do
88-
schmoozer = LongRunningSchmoozer.new(__dir__)
89-
schmoozer.echo("test")
90-
pid = schmoozer.pid
91-
end
104+
stdin = schmoozer.instance_variable_get(:@_schmooze_stdin)
105+
stdout = schmoozer.instance_variable_get(:@_schmooze_stdout)
106+
stderr = schmoozer.instance_variable_get(:@_schmooze_stderr)
107+
process_thread = schmoozer.instance_variable_get(:@_schmooze_process_thread)
108+
109+
# Create finalizer with parent's PID
110+
finalizer = LongRunningSchmoozer.create_finalizer(Process.pid, stdin, stdout, stderr, process_thread)
92111

93112
# Fork and try to run finalizer in child
94113
child_pid = fork do

test/garbage_test.rb

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
class GarbageTest < Minitest::Test
44
class GarbageSchmoozer < Schmooze::Base
55
method :test, 'function(){ return 1; }'
6+
7+
# Expose the finalizer for testing
8+
def self.create_finalizer(owner_pid, stdin, stdout, stderr, process_thread)
9+
finalize(owner_pid, stdin, stdout, stderr, process_thread)
10+
end
611
end
712

813
def test_process_is_not_started_until_used
@@ -13,17 +18,24 @@ def test_process_is_not_started_until_used
1318
end
1419

1520
def test_process_is_closed
16-
# Hacky way to test the finalizer. There is no way to guarantee that the
17-
# finalizer is called so instead we stub define_finalizer to call it immediately
18-
finalizer = nil
19-
ObjectSpace.stub :define_finalizer, proc {|s, p| finalizer = p} do
20-
garbage = GarbageSchmoozer.new(__dir__)
21-
garbage.test
22-
pid = garbage.pid
23-
finalizer.call
24-
assert_raises Errno::ESRCH do
25-
Process.kill(0, pid)
26-
end
21+
# Create an instance and get its internal state
22+
garbage = GarbageSchmoozer.new(__dir__)
23+
garbage.test
24+
pid = garbage.pid
25+
26+
# Get the internal process data
27+
stdin = garbage.instance_variable_get(:@_schmooze_stdin)
28+
stdout = garbage.instance_variable_get(:@_schmooze_stdout)
29+
stderr = garbage.instance_variable_get(:@_schmooze_stderr)
30+
process_thread = garbage.instance_variable_get(:@_schmooze_process_thread)
31+
32+
# Create and call the finalizer manually
33+
finalizer = GarbageSchmoozer.create_finalizer(Process.pid, stdin, stdout, stderr, process_thread)
34+
finalizer.call
35+
36+
# Verify the process was killed
37+
assert_raises Errno::ESRCH do
38+
Process.kill(0, pid)
2739
end
2840
end
2941
end

test/test_helper.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,11 @@
22
require 'schmooze'
33

44
require 'minitest/autorun'
5+
6+
# For minitest 6.x compatibility, try to require minitest/mock
7+
# This provides stub functionality
8+
begin
9+
require 'minitest/mock'
10+
rescue LoadError
11+
# minitest/mock not available, stub might still work in older versions
12+
end

0 commit comments

Comments
 (0)