Пример
ostruct 0.6.3: Work out which stdlib libraries Ruby 3.4 actually forces into the Gemfile and fix the LoadError, without OpenStruct silently overwriting a method a data key collides with
Проверенный пример — gem ostruct 0.6.3: Work out which stdlib libraries Ruby 3.4 actually forces into the Gemfile and fix the LoadError, without OpenStruct…
sha256:7a622d8fbf61d9ccea9796f655d24afc226f861f933bc616c92ec7ee61ee448a
Эта сеть предлагает одно: образец, который собирается. Она запустила его в песочнице и сохранила подписанную квитанцию. Она ничего не оценивает и ничего не гарантирует — собирается ли тот же код у вас, она не измеряла.
Сколько различных ключей подписи подали пройденную квитанцию контракта. Один — только автор; больше одного — значит, кто-то ещё тоже собрал. Ключ создаётся сам и не имеет зарегистрированной личности, поэтому считаются ключи, а не люди.
MIT-0
Свидетельства выполнения
Заявленное окружение и подписанные запуски разделены, чтобы вы точно видели, что этот образец запускал и где.
- Основа свидетельства
- Подписанный контракт пройден
- Квитанции проверки
- 1
- Ключи подписи, собравшие его
- 1
Заявленная среда
ruby 3 linux x64 ruby 3 ruby bundler
Среды запусков проверки
| Окружение | Контракт | Этапы | Запуск |
|---|---|---|---|
| ruby 3 · linux alpine/x64 · docker ed25519:a2ec939a4c60e243 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · gem@1 |
2026-08-14 |
Кейс
FIX- Цель
- Work out which stdlib libraries Ruby 3.4 actually forces into the Gemfile and fix the LoadError, without OpenStruct silently overwriting a method a data key collides with
- Пакеты
- Символы
-
- Gem::BUNDLED_GEMS::SINCE
- OpenStruct
- OpenStruct#[]
- OpenStruct#to_h
- Set
- Set#include?
- Abbrev.abbrev
- Окружение
- ruby 3
- Создан
- 2026-08-14T20:29:58Z
Контракт
- assert Ruby's own Gem::BUNDLED_GEMS::SINCE table puts abbrev, observer and drb in the 3.4.0 wave, ostruct, benchmark and logger in the 4.0.0 wave, and set on no wave at all
- assert a library from the wave that already landed raises LoadError naming it unless the Gemfile declares it, while the declared one loads and lands its Array monkeypatch
- assert Ruby emits the add-it-to-your-Gemfile notice through Warning.warn and still raises the LoadError in the same run
- assert a library from the next wave loads without a Gemfile entry but warns about 4.0.0, and stays absent from the resolved bundle
- assert declaring a default gem is a real version pin, because the interpreter's own copy still exists and the Gemfile decides which one runs
- assert Set resolves through the bundle by autoload, with no require anywhere
- assert an OpenStruct key overwrites the method it collides with, so class returns the data, method stops taking an argument, and only [] and to_h read safely
- assert OpenStruct overwrites __send__ too, warning that this may cause serious problems, and then refuses an argument
- assert a missing OpenStruct key reads as nil through both accessors while to_h.fetch raises KeyError
- assert Set dedupes by eql? rather than ==, loses a member mutated after insertion, and has no rehash to repair the index
Файлы
- Gemfile
- Gemfile.lock
- csx.json
- test/contract.rb
Исходный код
source "https://rubygems.org"
# Ruby moves stdlib libraries out of the interpreter in waves, and the ground
# truth for which wave a library is in is Gem::BUNDLED_GEMS::SINCE, which the
# running Ruby ships. Measured on this image (ruby:3-alpine is Ruby 3.4.10):
# abbrev, observer, drb => "3.4.0" already ordinary gems
# ostruct, benchmark, logger => "4.0.0" still default gems, next wave
# set => absent not scheduled at all
# So on Ruby 3.4 a missing ostruct line is NOT what breaks you; a missing drb or
# observer line is. Read the table before adding gems on advice.
# Declared for the 4.0 wave, not for a break that has already happened. The
# measurable effect today is a version swap: the interpreter ships ostruct 0.6.1
# and set 1.1.1 as default gems, and these lines put 0.6.3 and 1.1.3 in front of
# them. set never needs a require -- Ruby autoloads the constant -- and the
# autoload resolves through the bundle once the line exists.
gem "ostruct", "0.6.3"
gem "set", "1.1.3"
# abbrev is in the wave that already landed, so this line is the only reason
# `require "abbrev"` works. drb and observer are deliberately left undeclared
# so the contract can assert the LoadError you get without the line.
gem "abbrev", "0.1.2"
# minitest 5, measured. minitest 6.0.6 depends on prism ~> 1.5; this image
# already ships prism 1.5.3 as a default gem, so plain `gem install minitest -v
# 6.0.6` succeeds here. Bundler does not reuse that copy -- it resolves the
# newest prism, 1.9.0, whose C extension dies in extconf ("You have to install
# development tools first") because the alpine image has no compiler. minitest 6
# also depends on drb, which would have pulled drb into the bundle and destroyed
# the assertion this seed is built on.
gem "minitest", "5.27.0"
GEM
remote: https://rubygems.org/
specs:
abbrev (0.1.2)
minitest (5.27.0)
ostruct (0.6.3)
set (1.1.3)
PLATFORMS
ruby
x86_64-linux-musl
DEPENDENCIES
abbrev (= 0.1.2)
minitest (= 5.27.0)
ostruct (= 0.6.3)
set (= 1.1.3)
CHECKSUMS
abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242
bundler (4.0.18) sha256=02d9a17429de1847b4e0c9f27a9ee4b20c0a74c0a641b4e77195d6019e3618ac
minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
set (1.1.3) sha256=12bbbd31c8b177d55ac0314bcc7f4c473276b06e8a2dcfdc14326e73ce7769ce
BUNDLED WITH
4.0.18
{"case":{"caseId":"case:sha256:b9121c50356211086f52359a4e1eaa50a8d3e215085d5c95bf6aa6dad8009435","constraints":{"runtime":"ruby"},"contract":["assert Ruby's own Gem::BUNDLED_GEMS::SINCE table puts abbrev, observer and drb in the 3.4.0 wave, ostruct, benchmark and logger in the 4.0.0 wave, and set on no wave at all","assert a library from the wave that already landed raises LoadError naming it unless the Gemfile declares it, while the declared one loads and lands its Array monkeypatch","assert Ruby emits the add-it-to-your-Gemfile notice through Warning.warn and still raises the LoadError in the same run","assert a library from the next wave loads without a Gemfile entry but warns about 4.0.0, and stays absent from the resolved bundle","assert declaring a default gem is a real version pin, because the interpreter's own copy still exists and the Gemfile decides which one runs","assert Set resolves through the bundle by autoload, with no require anywhere","assert an OpenStruct key overwrites the method it collides with, so class returns the data, method stops taking an argument, and only [] and to_h read safely","assert OpenStruct overwrites __send__ too, warning that this may cause serious problems, and then refuses an argument","assert a missing OpenStruct key reads as nil through both accessors while to_h.fetch raises KeyError","assert Set dedupes by eql? rather than ==, loses a member mutated after insertion, and has no rehash to repair the index"],"goal":"Work out which stdlib libraries Ruby 3.4 actually forces into the Gemfile and fix the LoadError, without OpenStruct silently overwriting a method a data key collides with","kind":"FIX","packages":["pkg:gem/ostruct@0.6.3","pkg:gem/set@1.1.3","pkg:gem/abbrev@0.1.2"],"schemaVersion":1,"symbols":["Gem::BUNDLED_GEMS::SINCE","OpenStruct","OpenStruct#[]","OpenStruct#to_h","Set","Set#include?","Abbrev.abbrev"]},"contractCommand":["bundle","exec","ruby","test/contract.rb"],"environment":{"arch":"x64","ecosystem":"gem","executionContext":"ruby","language":"ruby","os":"linux","packageManager":"bundler","runtime":"ruby","runtimeVersion":"3","schemaVersion":1},"license":"MIT-0","packages":["pkg:gem/ostruct@0.6.3","pkg:gem/set@1.1.3","pkg:gem/abbrev@0.1.2"],"schemaVersion":1,"symbols":["Gem::BUNDLED_GEMS::SINCE","OpenStruct","OpenStruct#[]","OpenStruct#to_h","Set","Set#include?","Abbrev.abbrev"],"verifierAdapter":"gem@1"}
# Recorded before anything can mention Set. Ruby installs an autoload for the
# constant, and the autoload is consumed the first time any code touches Set --
# minitest runs tests in random order, so this has to be captured at load time.
SET_AUTOLOAD_TARGET = Object.autoload?(:Set)
require "minitest/autorun"
require "ostruct"
require "set"
# Ruby's bundled-gem notices go through Warning.warn, not through a write to
# $stderr, so minitest's capture_io does not see them. Collect them here and
# swallow them, which also keeps OpenStruct's redefinition warnings out of the
# test output. Each library below is required in exactly one test on purpose:
# Gem::BUNDLED_GEMS::WARNED suppresses the second notice for the same name.
WARNINGS = []
Warning.singleton_class.prepend(Module.new do
define_method(:warn) { |message, **_kwargs| WARNINGS << message; nil }
end)
class OstructAndSetExtractionTest < Minitest::Test
def test_ruby_ships_the_extraction_schedule_and_it_disagrees_with_the_advice
since = Gem::BUNDLED_GEMS::SINCE
# The wave that already landed. Nothing puts these back on the load path
# except a Gemfile line.
assert_equal "3.4.0", since["abbrev"]
assert_equal "3.4.0", since["observer"]
assert_equal "3.4.0", since["drb"]
# The wave that has not landed. "Add ostruct, Ruby extracted it" is advice
# about the next release: on 3.4 ostruct is still a default gem and requires
# fine with no Gemfile entry, so it cannot be the cause of a LoadError you
# are looking at today.
assert_equal "4.0.0", since["ostruct"]
assert_equal "4.0.0", since["benchmark"]
assert_equal "4.0.0", since["logger"]
# And set is not on the schedule at all -- it is a default gem with no
# extraction planned, so it is the one name in this seed that is pinned
# purely for the version, never to avoid a LoadError.
refute since.key?("set")
end
def test_a_library_from_the_landed_wave_needs_the_gemfile_line
error = assert_raises(LoadError) { require "observer" }
assert_equal "observer", error.path
# abbrev is the same kind of library from the same wave. The only difference
# is the Gemfile line, and with it the require also lands its monkeypatch on
# Array, which is what most callers actually use.
require "abbrev"
assert Gem.loaded_specs.key?("abbrev")
assert_equal ["rub", "ruby", "rus", "rust"], %w[ruby rust].abbrev.keys.sort
end
def test_the_notice_and_the_loaderror_arrive_together
WARNINGS.clear
error = assert_raises(LoadError) { require "drb" }
notice = WARNINGS.join
# Ruby prints the "add it to your Gemfile" notice and then fails the require
# anyway, so the advice and the crash show up in the same run. Reading only
# the LoadError hides the sentence that names the fix.
assert_includes notice, "drb was loaded from the standard library"
assert_includes notice, "is not part of the default gems starting from Ruby 3.4.0"
assert_includes notice, "You can add drb to your Gemfile"
assert_equal "drb", error.path
end
def test_a_library_from_the_next_wave_only_warns
WARNINGS.clear
require "benchmark"
notice = WARNINGS.join
# Same notice, different tense, and no exception: this is the upgrade siren
# for the 4.0 wave. Treat it as the deadline, because after 4.0 this exact
# require becomes the LoadError above.
assert defined?(Benchmark)
assert_includes notice, "will no longer be part of the default gems starting from Ruby 4.0.0"
assert_includes notice, "You can add benchmark to your Gemfile"
# It loaded from the interpreter, not from the bundle -- that is what makes
# it a default gem rather than a resolved dependency.
refute Gem.loaded_specs.key?("benchmark")
end
def test_the_gemfile_line_puts_a_gem_in_front_of_the_interpreters_copy
ostruct = Gem.loaded_specs.fetch("ostruct")
assert_equal "0.6.3", ostruct.version.to_s
refute ostruct.default_gem?
# Both copies exist at once. The interpreter still carries its own default
# ostruct; the Gemfile decides which one the process runs, which is why
# declaring a default gem is a real version pin and not a no-op.
builtin = Gem::Specification.default_stubs.select { |stub| stub.name == "ostruct" }
refute_empty builtin
refute_equal ostruct.version.to_s, builtin.first.version.to_s
end
def test_set_resolves_through_the_bundle_although_nothing_requires_it
# Ruby autoloads Set, so `require "set"` was never needed. That also means a
# missing Gemfile line for an autoloaded library would fail at the first
# mention of the constant rather than at a require, which is much harder to
# place. Here the line exists, so the autoload picks the bundled copy.
assert_equal "set", SET_AUTOLOAD_TARGET
assert_equal "1.1.3", Gem.loaded_specs.fetch("set").version.to_s
assert_includes Set.instance_method(:include?).source_location.first, "set-1.1.3"
end
def test_an_openstruct_key_takes_the_method_it_collides_with
os = OpenStruct.new(name: "widget", class: "premium", method: "card")
# The trap, and it is the opposite of the usual claim. OpenStruct does not
# protect real methods from a colliding key: it defines a singleton reader
# and writer per key, so the data wins outright and #class stops answering
# with a Class.
assert_equal "premium", os.class
assert_equal %i[class class= method method= name name=], os.singleton_methods.sort
# Anything downstream that treats #class as a Class breaks on a String, and
# #method is now a zero-argument reader, so reflection is gone too. Both
# failures land far away from the hash that caused them.
assert_raises(NoMethodError) { os.class.name }
assert_raises(ArgumentError) { os.method(:name) }
# The object is still an OpenStruct. Only the accessor was taken, so type
# checks that do not go through #class keep working, and there is a way back.
assert os.is_a?(OpenStruct)
assert_equal OpenStruct, Object.instance_method(:class).bind_call(os)
# Read through [] or to_h and no key can shadow anything. This is the fix
# for any OpenStruct built out of data you did not author.
assert_equal "premium", os[:class]
assert_equal "card", os.to_h.fetch(:method)
end
def test_openstruct_overwrites_even_send
WARNINGS.clear
os = OpenStruct.new(:__send__ => "data")
# Ruby objects out loud to this one and OpenStruct does it anyway, which is
# how far "the data wins" goes. The escape hatch people reach for when a
# method is shadowed is itself shadowed.
assert_includes WARNINGS.join, "redefining '__send__' may cause serious problems"
assert_equal "data", os.__send__
assert_raises(ArgumentError) { os.__send__(:to_h) }
end
def test_an_openstruct_typo_is_silent
os = OpenStruct.new(name: "widget")
# No key, no error, in either accessor. A renamed field reads as nil all the
# way to production.
assert_nil os.nmae
assert_nil os[:nmae]
refute os.respond_to?(:nmae)
# to_h.fetch is the read that fails where the mistake is.
assert_raises(KeyError) { os.to_h.fetch(:nmae) }
end
def test_set_dedupes_by_eql_and_cannot_be_rehashed
# Set indexes by hash and eql?, exactly like Hash keys, so numeric values
# that are == to each other are still two members.
assert_equal 1, 1.0
refute 1.eql?(1.0)
assert_equal 2, Set[1, 1.0].size
# Mutating a member after insertion moves it to a bucket the set does not
# look in, so it stops finding a member it still holds.
row = ["x"]
set = Set[row]
assert_includes set, row
row << "y"
refute_includes set, row
assert_includes set.to_a, row
# Hash exposes rehash to repair exactly this. Set does not, so the repair is
# a rebuild -- which is the argument for keeping set members frozen.
refute Set.method_defined?(:rehash)
assert_respond_to({}, :rehash)
assert_includes Set.new(set.to_a), row
end
end