Skip to content
Open
9 changes: 8 additions & 1 deletion .github/workflows/rubygems.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ jobs:
- ruby: { name: no symlinks, value: 4.0.5 }
os: { name: Windows, value: windows-2025 }
symlink: off

- ruby: { name: head (RUBY_BOX=1), value: head }
os: { name: Ubuntu, value: ubuntu-24.04 }
ruby_box: true
env:
RUBYGEMS_USE_PSYCH: ${{ matrix.use_psych || 'false' }}

Expand All @@ -79,7 +83,10 @@ jobs:
run: bin/rake setup
- name: Run Test
run: bin/rake test
if: matrix.ruby.name != 'truffleruby' && matrix.ruby.name != 'jruby' && matrix.symlink != 'off'
if: matrix.ruby.name != 'truffleruby' && matrix.ruby.name != 'jruby' && matrix.symlink != 'off' && !matrix.ruby_box
- name: Run Test (RUBY_BOX=1)
run: RUBY_BOX=1 bin/rake test
if: matrix.ruby_box
- name: Run Test isolatedly
run: bin/rake test:isolated
if: matrix.ruby.name == '3.4' && matrix.os.name != 'Windows'
Expand Down
2 changes: 1 addition & 1 deletion lib/bundler/gem_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def sh_with_status(cmd, &block)
Bundler.ui.debug(cmd)
SharedHelpers.chdir(base) do
outbuf = IO.popen(cmd, err: [:child, :out], &:read)
status = $?
status = Process.last_status
block&.call(outbuf) if status.success?
[outbuf, status]
end
Expand Down
3 changes: 2 additions & 1 deletion lib/rubygems/source/git.rb
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,10 @@ def rev_parse # :nodoc:
hash = Gem::Util.popen(git_command, "rev-parse", @reference).strip
end

# Process.last_status instead of $?, which Ruby::Box leaves uninitialized
raise Gem::Exception,
"unable to find reference #{@reference} in #{@repository}" unless
$?.success?
Process.last_status.success?

hash
end
Expand Down
9 changes: 7 additions & 2 deletions spec/bundler/shared_helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,11 @@

before do
ENV["RUBYOPT"] = "-r#{install_path}/bundler/setup"
allow(File).to receive(:expand_path).and_return("#{install_path}/bundler/setup")
# Only fake the resolution of bundler/setup itself. A blanket stub
# breaks unrelated RubyGems path lookups triggered lazily inside the
# example, see #set_rubyopt.
allow(File).to receive(:expand_path).and_call_original
allow(File).to receive(:expand_path).with("setup", anything).and_return("#{install_path}/bundler/setup")
allow(Gem).to receive(:bin_path).and_return("#{install_path}/bundler/setup")
end

Expand All @@ -403,7 +407,8 @@
let(:install_path) { "/opt/ruby with space/lib" }

before do
allow(File).to receive(:expand_path).and_return("#{install_path}/bundler/setup")
allow(File).to receive(:expand_path).and_call_original
allow(File).to receive(:expand_path).with("setup", anything).and_return("#{install_path}/bundler/setup")
allow(Gem).to receive(:bin_path).and_return("#{install_path}/bundler/setup")
end

Expand Down
15 changes: 14 additions & 1 deletion spec/support/command_execution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

module Spec
class CommandExecution
# Under RUBY_BOX, every spawned ruby prints an experimental warning to
# stderr, breaking specs that assert clean stderr.
RUBY_BOX_WARNING = Regexp.union(
/^[^\n]*: warning: Ruby::Box is experimental, and the behavior may change in the future!\n?/,
%r{^See https://docs\.ruby-lang\.org/\S+ for known issues, etc\.\n?}
)

def initialize(command, timeout:)
@command = command
@timeout = timeout
Expand Down Expand Up @@ -72,7 +79,13 @@ def failure?
attr_reader :failure_reason

def normalize(string)
string.dup.force_encoding(Encoding::UTF_8).scrub.strip.gsub("\r\n", "\n")
string = string.dup.force_encoding(Encoding::UTF_8).scrub.gsub("\r\n", "\n")
string = string.gsub(RUBY_BOX_WARNING, "") if ruby_box_enabled?
string.strip
end

def ruby_box_enabled?
defined?(Ruby::Box) && Ruby::Box.enabled?
end
end
end
30 changes: 30 additions & 0 deletions test/rubygems/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,10 @@ def setup
ENV["BUNDLE_USER_HOME"] = nil
ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] = "true"

# Child ruby processes inherit RUBY_BOX and print an experimental
# warning on startup, breaking assertions on subprocess stderr.
ENV["RUBYOPT"] = [ENV["RUBYOPT"], "-W:no-experimental"].compact.join(" ") if ruby_box_enabled?

@current_dir = Dir.pwd
@fetcher = nil

Expand Down Expand Up @@ -1367,6 +1371,32 @@ def ruby_repo?
!ENV["GEM_COMMAND"].nil?
end

##
# Is this test running under Ruby::Box (RUBY_BOX=1)?

def ruby_box_enabled?
defined?(Ruby::Box) && Ruby::Box.enabled?
end

##
# Ruby::Box gives each box detached copies of the stdio globals, so
# reassigning $stdout/$stderr cannot capture output written by Kernel#warn,
# Kernel#puts or subprocesses. Pends until the ruby-core fix for
# https://bugs.ruby-lang.org/issues/21867 lands.

def pend_for_ruby_box_stdio_capture
pend "Ruby::Box breaks $stdout/$stderr capture (https://bugs.ruby-lang.org/issues/21867)" if ruby_box_enabled?
end

##
# Under Ruby::Box, Marshal in the main box cannot resolve Gem:: (and other
# boxed) constants. Pends until the ruby-core fix for
# https://bugs.ruby-lang.org/issues/22090 lands.

def pend_for_ruby_box_marshal
pend "Marshal cannot resolve boxed constants under Ruby::Box (https://bugs.ruby-lang.org/issues/22090)" if ruby_box_enabled?
end

##
# Returns the make command for the current platform. For versions of Ruby
# built on MS Windows with VC++ or Borland it will return 'nmake'. On all
Expand Down
3 changes: 3 additions & 0 deletions test/rubygems/test_deprecate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def test_deprecated_method_calls_the_old_method
end

def test_deprecated_method_outputs_a_warning
pend_for_ruby_box_stdio_capture
out, err = capture_output do
thing = Thing.new
thing.foo
Expand Down Expand Up @@ -165,6 +166,7 @@ def execute
end

def test_deprecated_method_outputs_a_warning_old_way
pend_for_ruby_box_stdio_capture
out, err = capture_output do
thing = OtherThing.new
thing.foo
Expand All @@ -180,6 +182,7 @@ def test_deprecated_method_outputs_a_warning_old_way
end

def test_deprecated_method_when_class_overrides_format
pend_for_ruby_box_stdio_capture
out, err = capture_output do
thing = ThingWithFormat.new
thing.foo
Expand Down
3 changes: 2 additions & 1 deletion test/rubygems/test_exit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
class TestGemExit < Gem::TestCase
def test_exit
system(*ruby_with_rubygems_in_load_path, "-e", "raise Gem::SystemExitException.new(2)")
assert_equal 2, $?.exitstatus
# Process.last_status instead of $?, which Ruby::Box leaves uninitialized
assert_equal 2, Process.last_status.exitstatus
end

def test_status
Expand Down
2 changes: 2 additions & 0 deletions test/rubygems/test_gem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,7 @@ def test_self_try_activate_missing_prerelease
end

def test_self_try_activate_missing_extensions
pend_for_ruby_box_stdio_capture
spec = util_spec "ext", "1" do |s|
s.extensions = %w[ext/extconf.rb]
s.installed_by_version = v("2.2")
Expand Down Expand Up @@ -1346,6 +1347,7 @@ def test_setting_paths_does_not_mutate_parameter_object
end

def test_deprecated_paths=
pend_for_ruby_box_stdio_capture
stdout, stderr = capture_output do
Gem.paths = { "GEM_HOME" => Gem.paths.home,
"GEM_PATH" => [Gem.paths.home, "foo"] }
Expand Down
1 change: 1 addition & 0 deletions test/rubygems/test_gem_commands_build_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ def test_execute_strict_with_warnings
end

def test_execute_bad_spec
pend_for_ruby_box_stdio_capture
@gem.date = "2010-11-08"

gemspec_file = File.join(@tempdir, @gem.spec_name)
Expand Down
1 change: 1 addition & 0 deletions test/rubygems/test_gem_commands_open_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def gem(name, version = "1.0")
end

def test_execute
pend_for_ruby_box_stdio_capture
omit "JRuby on Windows spawns the editor with a different cwd" if Gem.win_platform? && Gem.java_platform?

@cmd.options[:args] = %w[foo]
Expand Down
1 change: 1 addition & 0 deletions test/rubygems/test_gem_commands_specification_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ def test_execute_file
end

def test_execute_marshal
pend_for_ruby_box_marshal
foo = util_spec "foo", "2"

install_specs foo
Expand Down
1 change: 1 addition & 0 deletions test/rubygems/test_gem_config_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ def test_handle_arguments_backtrace
end

def test_handle_arguments_debug
pend_for_ruby_box_stdio_capture
assert_equal false, $DEBUG

args = %w[--debug]
Expand Down
2 changes: 2 additions & 0 deletions test/rubygems/test_gem_dependency_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,7 @@ def test_install_version_default
end

def test_install_legacy_spec_with_nil_required_ruby_version
pend_for_ruby_box_marshal
path = File.expand_path "data/null-required-ruby-version.gemspec.rz", __dir__
spec = Marshal.load Gem.read_binary(path)
def spec.validate(*args); end
Expand All @@ -1128,6 +1129,7 @@ def spec.validate(*args); end
end

def test_install_legacy_spec_with_nil_required_rubygems_version
pend_for_ruby_box_marshal
path = File.expand_path "data/null-required-rubygems-version.gemspec.rz", __dir__
spec = Marshal.load Gem.read_binary(path)
def spec.validate(*args); end
Expand Down
2 changes: 1 addition & 1 deletion test/rubygems/test_gem_ext_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ def self.expand(val, config = CONFIG); val; end
system(Gem.ruby, "-rmkmf", "-e", "exit MakeMakefile::RbConfig::CONFIG['host_os'] == 'fake_os'",
"--", "--target-rbconfig=#{fake_rbconfig}")
end
unless $?.success?
unless Process.last_status.success?
assert_include(stderr, "uninitialized constant MakeMakefile::RbConfig")
pend "This version of mkmf does not support --target-rbconfig"
end
Expand Down
7 changes: 4 additions & 3 deletions test/rubygems/test_gem_ext_cargo_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ def test_full_integration
Open3.capture2e(*gem, "build", "rust_ruby_example.gemspec", "--output", built_gem)
Open3.capture2e(*gem, "install", "--verbose", "--local", built_gem, *ARGV)

stdout_and_stderr_str, status = Open3.capture2e(env_for_subprocess, *ruby_with_rubygems_in_load_path, "-rrust_ruby_example", "-e", "puts 'Result: ' + RustRubyExample.reverse('hello world')")
# Require inside -e because -r bypasses gem activation under RUBY_BOX=1
stdout_and_stderr_str, status = Open3.capture2e(env_for_subprocess, *ruby_with_rubygems_in_load_path, "-e", "require 'rust_ruby_example'; puts 'Result: ' + RustRubyExample.reverse('hello world')")
assert status.success?, stdout_and_stderr_str
assert_match "Result: #{"hello world".reverse}", stdout_and_stderr_str
end
Expand All @@ -134,7 +135,7 @@ def test_custom_name
Open3.capture2e(*gem, "install", "--verbose", "--local", built_gem, *ARGV)
end

stdout_and_stderr_str, status = Open3.capture2e(env_for_subprocess, *ruby_with_rubygems_in_load_path, "-rcustom_name", "-e", "puts 'Result: ' + CustomName.say_hello")
stdout_and_stderr_str, status = Open3.capture2e(env_for_subprocess, *ruby_with_rubygems_in_load_path, "-e", "require 'custom_name'; puts 'Result: ' + CustomName.say_hello")

assert status.success?, stdout_and_stderr_str
assert_match "Result: Hello world!", stdout_and_stderr_str
Expand Down Expand Up @@ -199,7 +200,7 @@ def skip_unsupported_platforms!
pend "jruby not supported" if Gem.java_platform?
pend "truffleruby not supported (yet)" if RUBY_ENGINE == "truffleruby"
system(@rust_envs, "cargo", "-V", out: IO::NULL, err: [:child, :out])
pend "cargo not present" unless $?.success?
pend "cargo not present" unless Process.last_status.success?
pend "ruby.h is not provided by ruby repo" if ruby_repo?
pend "rust toolchain of mingw is broken" if mingw_windows?
end
Expand Down
2 changes: 2 additions & 0 deletions test/rubygems/test_gem_package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,7 @@ def test_verify_corrupt
end

def test_verify_corrupt_tar_metadata_entry
pend_for_ruby_box_stdio_capture
gem = tar_file_header("metadata.gz", "", 0, 999, Time.now)

File.open "corrupt.gem", "wb" do |io|
Expand Down Expand Up @@ -1113,6 +1114,7 @@ def test_verify_corrupt_tar_checksums_entry
end

def test_verify_corrupt_tar_data_entry
pend_for_ruby_box_stdio_capture
gem = tar_file_header("data.tar.gz", "", 0, 100, Time.now)

File.open "corrupt.gem", "wb" do |io|
Expand Down
5 changes: 5 additions & 0 deletions test/rubygems/test_gem_package_tar_header_ractor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
end

class TestGemPackageTarHeaderRactor < Gem::Package::TarTestCase
def setup
super
pend "Ruby::Box ignores $VERBOSE=, so assert_ractor cannot keep the Ractor experimental warning out of the child stderr (ruby-core bug, same family as https://bugs.ruby-lang.org/issues/21867)" if ruby_box_enabled?
end

SETUP = <<~RUBY
header = {
name: "x",
Expand Down
2 changes: 2 additions & 0 deletions test/rubygems/test_gem_request_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def test_install_from_gemdeps
end

def test_install_from_gemdeps_explain
pend_for_ruby_box_stdio_capture
spec_fetcher do |fetcher|
fetcher.gem "a", 2
end
Expand All @@ -94,6 +95,7 @@ def test_install_from_gemdeps_explain
end

def test_install_from_gemdeps_explain_verbose
pend_for_ruby_box_stdio_capture
spec_fetcher do |fetcher|
fetcher.gem "a", 2
end
Expand Down
3 changes: 3 additions & 0 deletions test/rubygems/test_gem_request_set_gem_dependency_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def test_gem
end

def test_gem_duplicate
pend_for_ruby_box_stdio_capture
@gda.gem "a"

_, err = capture_output do
Expand Down Expand Up @@ -128,6 +129,7 @@ def test_gem_bitbucket_expand_path
end

def test_gem_git_branch
pend_for_ruby_box_stdio_capture
_, err = capture_output do
@gda.gem "a", git: "git/a", branch: "other", tag: "v1"
end
Expand All @@ -149,6 +151,7 @@ def test_gem_git_gist
end

def test_gem_git_ref
pend_for_ruby_box_stdio_capture
_, err = capture_output do
@gda.gem "a", git: "git/a", ref: "abcd123", branch: "other"
end
Expand Down
1 change: 1 addition & 0 deletions test/rubygems/test_gem_requirement.rb
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ def self.exploit(arg)
end

def test_marshal_load_attack
pend_for_ruby_box_marshal
wa = Gem::Net::WriteAdapter.allocate
wa.instance_variable_set(:@socket, self.class)
wa.instance_variable_set(:@method_id, :exploit)
Expand Down
10 changes: 9 additions & 1 deletion test/rubygems/test_gem_safe_marshal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,12 @@ def test_array_subclass
end

def test_frozen_object
pend_for_ruby_box_marshal
assert_safe_load_as Gem::Version.new("1.abc").freeze
end

def test_date
pend_for_ruby_box_marshal
assert_safe_load_as Date.new(1994, 12, 9)
end

Expand Down Expand Up @@ -369,6 +371,7 @@ def test_gem_spec_unmarshall_license
end

def test_gem_spec_unmarshall_required_ruby_rubygems_version
pend_for_ruby_box_marshal
spec = Gem::Specification.new do |s|
s.name = "hi"
s.version = "1.2.3"
Expand Down Expand Up @@ -500,7 +503,12 @@ def test_date_user_defined_rejected

def assert_safe_load_marshal(dumped, additional_methods: [], permitted_ivars: nil, equality: true, marshal_dump_equality: true,
inspect: true, to_s: true)
loaded = Marshal.load(dumped)
loaded = begin
Marshal.load(dumped)
rescue ArgumentError => e
pend_for_ruby_box_marshal if e.message.include?("undefined class/module")
raise
end
safe_loaded =
assert_nothing_raised("dumped: #{dumped.b.inspect} loaded: #{loaded.inspect}") do
if permitted_ivars
Expand Down
Loading