How to access constant value from spec file? - ruby

I get a uninitialized constant error, when I run this rspec by rspec foo_spec.rb.
# foo.rb
class Foo
FILENAME = "filename.txt"
def filename
FILENAME
end
end
# foo_spec.rb
require_relative 'foo'
describe Foo do
describe "#filename" do
it "should have right filename" do
foo = Foo.new
expect(foo.filename).to eq FILENAME
end
end
end
I confirmed if I change FILENAME to "filename.txt", the test passes.
How should I use constant value with rspec?

This isn't an rspec problem, it's a Ruby problem. You need to qualify the constant with the class in which its declared. FILENAME should be Foo::FILENAME.
You can only refer to it by FILENAME within the context of Foo. Otherwise, supposing I had a class Bar which also defined a constant called FILENAME, how could Ruby figure out which one I was referring to?

Related

Module classes not visible in Minitest describe

When I want to include a module into a Minitest/spec test, I can access the functions from the module, but not the classes defined in it. Example:
module Foo
def do_stuff
end
class Bar
end
end
x=describe Foo do
include Foo
end
p x.constants # shows :Bar
describe Foo do
include Foo
it "foos" do
do_stuff # works
Bar.new # raises a NameError
end
end
Running this snippet gives me a "NameError: uninitialized constant Bar", however, the p x.constantsshows that Bar is defined. I looked into the Minitest source code for describe and it uses class_eval on the block in the context of some anonymous class. When I do that in the context of a normal class it works fine and I can access Bar. Why doesn't it work with describe/it or what do I have to do in order to access the classes directly?
EDIT:
Interestingly, if you call class_eval directly on some class the included class Bar can be found, e.g.
class Quux
def it_foos
do_stuff # works
Bar.new # does NOT raise a NameError
end
end
Quux.class_eval do
include Foo
end
Quux.new.it_foos
won't throw a NameError...
If you check the documentation for #class_eval (for example, https://ruby-doc.org/core-2.5.0/Module.html#method-i-class_eval) you will see the answer there: "Evaluates the string or block in the context of mod, except that when a block is given, constant/class variable lookup is not affected".
So, include within class_eval simply doesn't affect constants resolution.
As far as I understand from the short look at minitest's source code, describe internally creates a new anonymous class (let's name it C) and casts class_eval on it with the block you provide. During this call its create the respective test instance methods that are executed later. But include doesn't affect constants resolution for C, so Bar stays unknown.
There is an obvious (and quite ugly) solution - the following should work because you include Foo into outer context, so Bar goes into lexical scope accessible for describe:
include Foo
describe Foo do
it "foos" do
do_stuff
Bar.new
end
end
But tbh I'd avoid such code. Probably it's better to set up the class mock explicitly, smth like
module Foo
def do_stuff
"foo"
end
class Bar
def do_stuff
"bar"
end
end
end
...
describe Foo do
let(:cls) { Class.new }
before { cls.include(Foo) }
it "foos" do
assert cls.new.do_stuff == "foo"
end
it "bars" do
assert cls::Bar.new.do_stuff == "bar"
end
end
(but take pls the latter with a grain of salt - I almost never use Minitest so have no idea of its "common idioms")

Ruby metaprogramming: define_method block not maintaining scope

I'm working on dynamically patching a bunch of classes and methods(most of the time these methods are not simple "puts" like a lot of examples I've been able to find on the internet)
Say for instance I have the following code:
foo.rb
module Base
class Foo
def info
puts 'Foo#info called'
end
end
end
& I also have the following class:
test.rb
module Base
class Test
def print
puts "Test#print called"
Foo.new.info
end
end
end
Then in main.rb I have the following where I want to add a method that uses a class within the same module(Foo in this case)
require_relative './foo'
require_relative './test'
new_method_content = "puts 'hi'
Foo.new.info"
Base::Test.instance_eval do
def asdf
puts "Test#asdf called"
Foo.new.info
end
end
Which, when executed will net the following:
Uncaught exception: uninitialized constant Foo
Which sort of makes sense to me because the main.rb file doesn't know that I want Base::Foo, however, I need a way to maintain lookup scope because Base::Test should be able to find the class Foo that I want.
Base::Test.instance_eval do
def asdf
puts "Test#asdf called"
Foo.new.info
end
end
I've done a fair bit of googling and SO'ing but haven't found anything about how to maintain constant lookup scope while class_eval/instance_eval/module_eval/define_method(I've tried a lot of Ruby's dark magic methods all of which have ended in varying degrees of failure lol)
https://cirw.in/blog/constant-lookup
Confusingly however, if you pass a String to these methods, then the String is evaluated with Module.nesting containing just the class itself (for class_eval) or just the singleton class of the object (for instance_eval).
& also this:
https://bugs.ruby-lang.org/issues/6838
Evaluates the string or block in the context of mod, except that when
a block is given, constant/class variable lookup is not affected.
So my question is:
How can I redefine a method BUT maintain constant/class scope?
I've been trying a bunch of other things(in the context of main.rb):
Base::Test.class_eval('def asdf; puts "Test#asdf called"; Foo.new.info; end')
Base::Test.new.asdf
=>
Test#asdf called
Uncaught exception: uninitialized constant Base::Test::Foo
Did you mean? Base::Foo
(which is a diff problem in that it's trying to look it up from the evaluated module nesting? I'm not sure why it doesn't try all module paths available from Base::Test though, I would think it would try Base::Test::Foo which doesn't exist, so then it would go up the module tree looking for the class(Base::Foo) which would exist)
When you reference the class Base::Test like this, ruby does not take the Base:: as the module context to look up constants. That is the normal behaviour and would also not work, if you would define the moethod directly.
But you could do it in this way:
module Base
Test.instance_eval do
def asdf
puts "Test#asdf called"
Foo.new.info
end
end
end

Return the last evaluated object from a file

Is it possible to return the last evaluated object from a Ruby file?
Suppose I have a file like:
# app.rb
def foo
"Hello, world!"
end
foo
Then, I would expect something like this behavior:
# other_file.rb
require_relative!('foo') # => "Hello, world!"
Instead of the require_relative's true returned value, we fetch the last evaluated object of the required file.
Is there a way to have a require_relative!-like behavior?
It sounds like a bad practice. Ruby files are storages of ruby code, not data.
Idiomatically you should create a Ruby file with class or module, require it and call some function from it.
# foo.rb
module Foo
extend self
def foo
"bar"
end
end
# irb
require 'foo'
Foo.foo
#=> bar
Otherwise, as #sawa mentioned, you should read file as a string and eval it. Which is idiomatically wrong.

Create a class instance from string when files containing class are 'required' run-time in Ruby?

I am writing some test cases in ruby using Minitest FW. In the setup routine, I try to require all the ruby files which have the classes of which I wish to create instances (by string name) in each test case. I get the error saying uninitialized constant 'class name'. I can understand that this might not be the best practice, However, I am curious and want to know what I doing wrong. Below is the code I am working on.
# Native.rb
require 'minitest/autorun'
class Native_Test < Minitest::Unit::TestCase
def setup
path = Dir.getwd
Dir[File.join(path + '*test.rb')].each {|file| require file} # requiring all files
end
def test_tc1
puts 'Hi'
method_name = 'verifyLogic1'
class_name = 'My_work1'
obj = Object.const_get(class_name).new # -> undefined constant My_work1
obj.method_name('1','2')
end
def test_tc2
..
end
end
# validate_1_test.rb
class My_work1
def verifyLogic1(arg1,arg2)
puts 'arg1'
puts 'arg2'
end
end
Please correct me wherever I am wrong. More than happy to accept any inputs.
Firstly:
path + '*test.rb'
This will end up with: current/folder*test.rb. Instead do:
File.join path, '*test.rb'
Secondly, this:
obj.method_name('1','2')
will not work, as you will try to execute method method_name on the object. You need to use send or even better `public_send':
obj.public_send(method_name, '1', '2')
Few more notes: There are 3 types of common naming conventions in ruby. CamelCase, snake_case or ALL_CAPITAL. CamelCase should be used for class names, snake_case for variables and methods and ALL_CAPITALS for other constants (unless like me you prefer to use CamelCase for all constants).

Is it possible to give a sub-module the same name as a top-level class?

Background:
ruby thinks I'm referencing a top-level constant even when I specify the full namespace
How do I refer to a submodule's "full path" in ruby?
Here's the problem, distilled down to a minimal example:
# bar.rb
class Bar
end
# foo/bar.rb
module Foo::Bar
end
# foo.rb
class Foo
include Foo::Bar
end
# runner.rb
require 'bar'
require 'foo'
➔ ruby runner.rb
./foo.rb:2: warning: toplevel constant Bar referenced by Foo::Bar
./foo.rb:2:in `include': wrong argument type Class (expected Module) (TypeError)
from ./foo.rb:2
from runner.rb:2:in `require'
from runner.rb:2
Excellent; your code sample is very clarifying. What you have there is a garden-variety circular dependency, obscured by the peculiarities of Ruby's scope-resolution operator.
When you run the Ruby code require 'foo', ruby finds foo.rb and executes it, and then finds foo/bar.rb and executes that. So when Ruby encounters your Foo class and executes include Foo::Bar, it looks for a constant named Bar in the class Foo, because that's what Foo::Bar denotes. When it fails to find one, it searches other enclosing scopes for constants named Bar, and eventually finds it at the top level. But that Bar is a class, and so can't be included.
Even if you could persuade require to run foo/bar.rb before foo.rb, it wouldn't help; module Foo::Bar means "find the constant Foo, and if it's a class or a module, start defining a module within it called Bar". Foo won't have been created yet, so the require will still fail.
Renaming Foo::Bar to Foo::UserBar won't help either, since the name clash isn't ultimately at fault.
So what can you do? At a high level, you have to break the cycle somehow. Simplest is to define Foo in two parts, like so:
# bar.rb
class Bar
A = 4
end
# foo.rb
class Foo
# Stuff that doesn't depend on Foo::Bar goes here.
end
# foo/bar.rb
module Foo::Bar
A = 5
end
class Foo # Yep, we re-open class Foo inside foo/bar.rb
include Bar # Note that you don't need Foo:: as we automatically search Foo first.
end
Bar::A # => 4
Foo::Bar::A # => 5
Hope this helps.
Here is a more minimal example to demonstrate this behavior:
class Bar; end
class Foo
include Foo::Bar
end
Output:
warning: toplevel constant Bar referenced by Foo::Bar
TypeError: wrong argument type Class (expected Module)
And here is even more minimal:
Bar = 0
class Foo; end
Foo::Bar
Output:
warning: toplevel constant Bar referenced by Foo::Bar
The explanation is simple, there is no bug: there is no Bar in Foo, and Foo::Bar is not yet defined. For Foo::Bar to be defined, Foo has to be defined first. The following code works fine:
class Bar; end
class Foo
module ::Foo::Bar; end
include Foo::Bar
end
However, there is something that is unexpected to me. The following two blocks behave differently:
Bar = 0
class Foo; end
Foo::Bar
produces a warning:
warning: toplevel constant Bar referenced by Foo::Bar
but
Bar = 0
module Foo; end
Foo::Bar
produces an error:
uninitialized constant Foo::Bar (NameError)
Here's another fun example:
module SomeName
class Client
end
end
module Integrations::SomeName::Importer
def perform
...
client = ::SomeName::Client.new(...)
...
end
end
That produces:
block in load_missing_constant': uninitialized constant Integrations::SomeName::Importer::SomeName (NameError)
Ruby (2.3.4) just goes to the first occurrence of "SomeName" it can find, not to the top-level.
A way to get around it is to either use better nesting of modules/classes(!!), or to use Kernel.const_get('SomeName')

Resources