|
| 1 | +require_relative "test_helper" |
| 2 | + |
| 3 | +# Guards against "constant drift" between the RBS core signatures and the actual |
| 4 | +# runtime, in both directions: |
| 5 | +# |
| 6 | +# * a constant declared in RBS but no longer defined by Ruby (e.g. |
| 7 | +# `Float::ROUNDS`, removed in Ruby 3.0), and |
| 8 | +# * a constant defined by Ruby but missing from RBS. |
| 9 | +# |
| 10 | +# Only platform- and build-invariant core classes are hard-gated here. Classes |
| 11 | +# whose constant set legitimately varies by OS or build options (`Process`, |
| 12 | +# `Socket`, `Errno`, `Signal`, `File::Constants`, `RbConfig`, `Etc`, ...) are |
| 13 | +# intentionally excluded: their RBS declarations cannot match any single |
| 14 | +# platform's runtime. `Object`/`BasicObject`/`Kernel` are excluded too, since |
| 15 | +# every top-level class shows up under `Object.constants`. |
| 16 | +class ConstantDriftTest < Test::Unit::TestCase |
| 17 | + # Platform/build-invariant core classes and modules whose declared constant |
| 18 | + # set must match the runtime exactly. |
| 19 | + HARD_GATE = [ |
| 20 | + Float, Integer, Numeric, Rational, Complex, |
| 21 | + Math, Comparable, |
| 22 | + String, Symbol, |
| 23 | + Array, Hash, Range, Struct, |
| 24 | + NilClass, TrueClass, FalseClass |
| 25 | + ].freeze |
| 26 | + |
| 27 | + # Known, intentional exceptions keyed by "::Name" => [:CONST, ...]. Use this |
| 28 | + # for build- or platform-conditional constants (and `private_constant`s) that |
| 29 | + # are legitimately undeclared, so the gate stays green across CI platforms |
| 30 | + # without being weakened elsewhere. |
| 31 | + SKIP = { |
| 32 | + # Defined only when Ruby is built with GMP (USE_GMP): present on the Linux |
| 33 | + # CI build, absent on e.g. macOS. |
| 34 | + "::Integer" => [:GMP_VERSION] |
| 35 | + }.freeze |
| 36 | + |
| 37 | + def env |
| 38 | + StdlibTest::DEFAULT_ENV |
| 39 | + end |
| 40 | + |
| 41 | + # Constants declared directly under `type_name` in the loaded RBS environment |
| 42 | + # (plain constants plus nested classes/modules and their aliases), matching |
| 43 | + # what `Module#constants(false)` returns at runtime. |
| 44 | + def rbs_constants(type_name) |
| 45 | + prefix = "#{type_name}::" |
| 46 | + names = [] |
| 47 | + [env.constant_decls, env.class_decls, env.class_alias_decls].each do |store| |
| 48 | + store.each_key do |tn| |
| 49 | + s = tn.to_s |
| 50 | + next unless s.start_with?(prefix) |
| 51 | + |
| 52 | + rest = s.delete_prefix(prefix) |
| 53 | + names << rest.to_sym unless rest.include?("::") |
| 54 | + end |
| 55 | + end |
| 56 | + names.uniq.sort |
| 57 | + end |
| 58 | + |
| 59 | + HARD_GATE.each do |klass| |
| 60 | + define_method(:"test_no_constant_drift_#{klass.name.gsub("::", "_")}") do |
| 61 | + type_name = "::#{klass.name}" |
| 62 | + skip = SKIP[type_name] || [] |
| 63 | + runtime = (klass.constants(false) - skip).sort |
| 64 | + declared = (rbs_constants(type_name) - skip).sort |
| 65 | + |
| 66 | + stale = declared - runtime |
| 67 | + missing = runtime - declared |
| 68 | + |
| 69 | + assert_empty stale, |
| 70 | + "RBS declares #{type_name} constants that no longer exist at runtime: #{stale.inspect}. " \ |
| 71 | + "Remove them from the signature (or add to ConstantDriftTest::SKIP if intentional)." |
| 72 | + assert_empty missing, |
| 73 | + "Runtime defines #{type_name} constants missing from RBS: #{missing.inspect}. " \ |
| 74 | + "Add them to the signature (or add to ConstantDriftTest::SKIP if intentional)." |
| 75 | + end |
| 76 | + end |
| 77 | +end |
0 commit comments