Skip to content

Sandbox: clear_extra_constants removes C-extension constants loaded via require #14

Description

@jcouball

Summary

clear_extra_constants is supposed to preserve constants introduced by require calls during example evaluation, but it fails for constants defined in C extensions (.so/.bundle files). Module#const_source_location returns [false, 0] for C-defined constants. Because false is falsy, the guard source_file && skip_if_loaded_by.include?(source_file) short-circuits and the constant is removed.

On subsequent examples, require is a no-op (the file is already in $LOADED_FEATURES), so the constant stays gone, causing NameError.

Location

lib/yard_example_test/example/constant_sandbox.rbclear_extra_constants

def clear_extra_constants(scope, before, skip_if_loaded_by: [])
  (scope.constants - before).each do |constant|
    if skip_if_loaded_by.any?
      source_file, = scope.const_source_location(constant.to_s)
      next if source_file && skip_if_loaded_by.include?(source_file)
    end
    scope.__send__(:remove_const, constant)
  end
end

const_source_location return values

Constant type Return value source_file
Ruby source file ["/path/to/file.rb", 10] "/path/to/file.rb" (truthy)
C extension [false, 0] false (falsy)
Autoloaded (not yet loaded) [nil, nil] nil (falsy)
Built-in nil N/A (destructure gives nil)

Concrete example

# @example first
#   require 'json'
#   JSON.parse('{}') #=> {}
#
# @example second
#   JSON.parse('[]') #=> []  # NameError: uninitialized constant JSON

After the first example, JSON (defined by a C extension) is removed from Object even though json was loaded via require. The second example fails because require 'json' is a no-op (already in $LOADED_FEATURES) and JSON is gone.

Proposed solution

Treat false from const_source_location as "defined in a native extension" and preserve the constant. One approach:

def clear_extra_constants(scope, before, skip_if_loaded_by: [])
  (scope.constants - before).each do |constant|
    if skip_if_loaded_by.any?
      location = scope.const_source_location(constant.to_s)
      source_file = location&.first
      # false means C extension — preserve it; nil/missing means dynamic
      next if source_file == false
      next if source_file && skip_if_loaded_by.include?(source_file)
    end
    scope.__send__(:remove_const, constant)
  end
end

Acceptance criteria

  • Constants defined by C extensions loaded via require during example evaluation are preserved across examples
  • Constants defined by Ruby files loaded via require are still preserved (existing behavior)
  • Constants defined inline in example code (not via require) are still cleaned up
  • All existing cucumber scenarios continue to pass

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions