Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,33 @@
require "pathname"
TEST_IMAGES_DIR = Pathname.new(File.expand_path("fixtures/images", __dir__))

# The child environment for a subprocess probe that chdirs OUT of the project
# to exercise a load path it built itself. Such a probe must not inherit this
# repo's development bundle, and dropping these four is the whole of it:
#
# - BUNDLER_SETUP is the one that bit us. RubyGems ends rubygems.rb with
# `require ENV["BUNDLER_SETUP"] if ENV["BUNDLER_SETUP"] && !defined?(Bundler)`,
# so ANY child re-enters bundler/setup before it reaches `-e`. That resolves
# BUNDLE_GEMFILE -- which .github/workflows/test.yml sets to the RELATIVE
# `gemfiles/<gemfile>` -- against the child's cwd, i.e. the tmpdir, and the
# child dies with Bundler::GemfileNotFound before running a line of the probe.
# Bundler >= 2.6 rewrites BUNDLE_GEMFILE to an absolute path during setup and
# the 2.5 that ships with ruby 3.3 does not, so this was green everywhere but
# the 3.3 cells -- and invisible under `bundle exec`, which expands it too.
# - RUBYOPT carries `-r<abs>/bundler/setup`, the same door.
# - RUBYLIB and BUNDLE_GEMFILE inject the development load path directly.
#
# Scrubbing is not sufficient on its own -- the probe must ALSO chdir out of
# the project, or RubyGems finds gems.rb and puts all of it back. Probes that
# stay inside the project root (`chdir: PROJECT_ROOT`) genuinely want the
# bundle and must NOT use this.
PROBE_ENV = {
"BUNDLER_SETUP" => nil,
"BUNDLE_GEMFILE" => nil,
"RUBYLIB" => nil,
"RUBYOPT" => nil
}.freeze

require "support/setup_rails_app"
require "minitest/autorun"

Expand Down
34 changes: 32 additions & 2 deletions test/unit/gem_name_entry_point_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
class GemNameEntryPointTest < ActiveSupport::TestCase
LIB = File.expand_path("../../lib", __dir__)

# Opens every probe, ahead of the minitest gate below: the point of this
# probe is a CONSUMER's bundle, so inheriting ours proves nothing about
# either. See PROBE_ENV in test_helper.rb for what leaks and how.
NO_BUNDLE_GATE = <<~RUBY
abort("GATE: the probe inherited the development bundle, so it proves nothing") if defined?(Bundler)
RUBY

# Opens every probe: a probe that quietly tested the wrong environment
# would "prove" whatever we hoped it would.
GATE = {
Expand Down Expand Up @@ -54,8 +61,12 @@ class GemNameEntryPointTest < ActiveSupport::TestCase
# scratch Gemfile reproduces the same thing and was used to confirm this
# one, but it needs the network and has no place in the unit suite.)
#
# The cwd is half of that; PROBE_ENV is the other half, and the two only
# work together -- see its comment in test/test_helper.rb. +env+ is a seam
# for the gate test below, which restores a leak on purpose.
#
# @return [Array(String, String, Process::Status)] stdout, stderr, status
def self.probe(script, minitest: true)
def self.probe(script, minitest: true, env: PROBE_ENV)
Dir.mktmpdir do |dir|
load_paths = ["-I#{LIB}"]

Expand All @@ -68,7 +79,7 @@ def self.probe(script, minitest: true)
load_paths.unshift("-I#{shim}")
end

Open3.capture3(RbConfig.ruby, *load_paths, "-e", GATE.fetch(minitest) + script, chdir: dir)
Open3.capture3(env, RbConfig.ruby, *load_paths, "-e", NO_BUNDLE_GATE + GATE.fetch(minitest) + script, chdir: dir)
end
end

Expand Down Expand Up @@ -98,6 +109,25 @@ def self.probe(script, minitest: true)
assert_match(/require: false/, err, "the message must name the way to silence it")
end

# The gate line has to be able to FAIL, or it is a comment with an `if`
# around it. Restore the ONE variable the scrub exists to drop -- RubyGems
# turns BUNDLER_SETUP back into a `require "bundler/setup"` in any child --
# and the same probe must refuse to run. Restoring it alone (BUNDLE_GEMFILE
# still scrubbed) is what makes this deterministic on every ruby: the child
# boots far enough to reach the gate instead of dying at Bundler's own
# GemfileNotFound, which is what CI saw and which only reproduces on the
# bundler that leaves a relative BUNDLE_GEMFILE alone.
test "the no-bundle gate rejects a probe that inherited the development bundle" do
bundler_setup = ENV["BUNDLER_SETUP"]
assert bundler_setup, "this test only means something when the suite itself runs under bundler"

out, err, status = self.class.probe(%(puts "RAN"), env: PROBE_ENV.merge("BUNDLER_SETUP" => bundler_setup))

refute_predicate status, :success?, "the gate let an inherited bundle through:\n#{out}\n#{err}"
assert_match(/GATE: the probe inherited the development bundle/, err)
refute_includes out, "RAN", "the gate must abort BEFORE the probe body runs"
end

# The other direction: the documented zero-require Rails path.
test "the gem-name entry point still auto-activates the Minitest assertions when minitest is present" do
out, err, status = self.class.probe(<<~'RUBY')
Expand Down
8 changes: 4 additions & 4 deletions test/unit/legacy_deletion_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,9 @@ def in_deleted_tree

# A fresh process with ONLY +tree+ on the load path.
#
# `chdir: tree` is the load-bearing half, and NOT a detail. Scrubbing
# RUBYOPT/BUNDLE_GEMFILE is not sufficient on its own: with the cwd still
# inside the project, RubyGems auto-discovers gems.rb, puts
# `chdir: tree` is the load-bearing half, and NOT a detail. PROBE_ENV is not
# sufficient on its own: with the cwd still inside the project, RubyGems
# auto-discovers gems.rb, puts
# `-rbundler/setup` BACK into RUBYOPT, and the gemspec unshifts the real
# lib/ ahead of the -I dir -- measured, this exact scrub with cwd at the
# project root loads 24 files from the intact tree. Running from the
Expand All @@ -208,7 +208,7 @@ def probe(tree, script)
$LOAD_PATH.unshift(#{tree.inspect})
#{SupportLoadProbeTest::CUCUMBER_RUNTIME_STUB}
RUBY
env = {"DELETED_TREE" => tree, "RUBYOPT" => nil, "BUNDLE_GEMFILE" => nil, "RUBYLIB" => nil}
env = PROBE_ENV.merge("DELETED_TREE" => tree)
out, status = Open3.capture2e(env, RbConfig.ruby, "-e", preamble + script, chdir: tree)

"#{script.lines.first.strip} -> #{out}" unless status.success?
Expand Down
Loading