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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions