How to access "global" (constant?) capistrano variables from classes? (ruby) - ruby

So my deploy.rb script in capistrano starts like this, which I guess is pretty normal:
require 'capistrano/ext/multistage'
require 'nokogiri'
require 'curb'
require 'json'
# override capistrano defaults
set :use_sudo, false
set :normalize_asset_timestamps, false
# some constant of mine
set :my_constant, "foo_bar"
Later, I can access my constant in functions or tasks within namespaces, like:
namespace :mycompany do
def some_function()
run "some_command #{my_constant}"
end
desc <<-DESC
some task description
DESC
task :some_task do
run "some_command #{my_constant}"
end
end
However, if I use the constant in a class, like this:
namespace :mycompany do
class SomeClass
def self.some_static_method()
run "some_command #{my_constant}"
end
end
end
It fails with:
/config/deploy.rb:120:in `some_static_method': undefined local variable or method `my_constant' for #<Class:0x000000026234f8>::SomeClass (NameError)
What am I doing wrong??
Thanks

The deploy.rb file is instance_evaled, this means it's being executed inside the context of an object, and as such anything you declare will be available until you leave that context. As soon as you create a class that provides a new context.
In order to access the original context you have to pass the object (self) to the class method.
namespace :mycompany do
class SomeClass
def self.some_static_method(cap)
run "some_command #{cap.fetch(:my_constant)}"
end
end
SomeClass.some_static_method(self)
end
Although I really don't understand why you are declaring a class like this, it's an odd place for it.

Related

Ruby Module Constant Set and Read

I'm developing a module that contains standard methods as well as CLI (Thor) and API (Sinatra). I've created a Submodule that includes a class that inherits Sinatra::Base:
module Monteverde
module Activity
def self.version
"1.0.1"
end
class API < Sinatra::Base
set :port, 22340
get "/version" do
Monteverde::Activity.version
end
run! if defined? Monteverde::OPERATION
end
end
end
This "Activity" is invokes from the CLI (Thor):
module Monteverde
class CLI < Thor
desc "api", "Start Activity API"
def api
Monteverde.const_set("OPERATION", "started")
Monteverde::Activity::API
end
desc "aversion", "Get Activity Version"
def aversion
puts Monteverde::Activity.version
end
end
end
If I don't add an "if" to Sinatra's run! it will run automatically and take over the rest of the methods in the module:
...
class API < Sinatra::Base
register Sinatra::DefaultRoutes
set :port, 22340
get "/version" do
Monteverde::Activity.version
end
run!
end
$ ruby monteverde.rb aversion
$ == Sinatra (v2.0.0) has taken the stage on 22340 for development with backup from Puma
$ ...
My issue is that the OPERATION constant is not recognized even though it's set right before the module is called.
Why isn't OPERATION being recognized and how can I get it to be?
Is there another more intuitive/useful way to get Sinatra not to fire when I call the module?
In your API class definition, the run! line happens as soon as you require that file. It doesn't run again if you reference the class, as you're trying to do with the last line of your def api method.
To get around this, you can move the run! command into a method, and call that from Thor:
class API < Sinatra::Base
# ... other stuff
def self.start
run! if defined? Monteverde::OPERATION
end
end
Then in the Thor file:
def api
Monteverde.const_set("OPERATION", "started")
Monteverde::Activity::API.start
end
You can deduce that run! is a class method since it's callable in the scope of the class definition (not in an instance method), and so I define a class method to call it.
It would be simpler, though, to not define def self.start, and instead just call the run! method from the Thor file directly:
def api
Monteverde::Activity::API.run!
end

Handling a method from outside of a mixin module in ruby and rspec

I have a class that looks like this:
module ReusableBitlyLinks
def shorten_url url, *args
ShortenedUrl.shorten_url_with_bitly( url, email.user )
end
end
I have a test that looks like this:
require File.expand_path("../../../../app/decorators/mixins/reusable_bitly_links", __FILE__)
include ReusableBitlyLinks
describe ReusableBitlyLinks do
describe "shorten_url" do
it "works" do
ReusableBitlyLinks.shorten_url('asdf').should == 'asdf'
end
end
end
When I run the test I get an error that says:
uninitialized constant ReusableBitlyLinks::ShortenedUrl
How do I mock stub ReusableBitlyLinks::ShortenedUrl?
Is ShortenedUrl defined inside ReusableBitlyLinks module? If not - try to access it with ::ShortenedUrl.shorten_url_with_bitly.
Not sure what stubbing has to do with it...
In your mixin module, you need to tell it that ShortenedUrl is from outside the module by prepending :::
module ReusableBitlyLinks
def shorten_url(url, *args)
::ShortenedUrl.shorten_url_with_bitly(url, email.user)
end
end
You may also need to do a require inside the mixin file to load whatever file it is that defines ShortenedUrl.
Further, in your test file, the line:
require File.expand_path("../../../../app/decorators/mixins/reusable_bitly_links", __FILE__)
could be simplified to:
require_relative '../../../../app/decorators/mixins/reusable_bitly_links'

Adding a class in a module to Cucumber World

Given I have defined the following modules in my features/support directory
apiworld.rb
module Api
class User
...
end
...
end
and also
webworld.rb
module Web
class User
...
end
...
end
in my env.rb file I have
env.rb
require File.expand_path(File.dirname(__FILE__)+'/webworld')
require File.expand_path(File.dirname(__FILE__)+'/apiworld')
if ENV['USE_API'] == 1
World(Api)
else
World(Web)
end
So if I try to use this construct in a step definition like
Given /^a user is created$/ do
#user = User.new
end
And run cucumber, my ruby interpreter will give me the this output
uninitialized constant User (NameError)
./features/step_definitions/user_steps.rb:17: [...]
How to make this work? Is there a way or am I thinking i the wrong direction. I am pretty new to ruby - so I don't really know what it can do and what it can not do.
You can't use World for this. World is for mixing methods into the self object in each stepdef.
Instead of this:
if ENV['USE_API'] == 1
World(Api)
else
World(Web)
end
Try this:
User = ENV['USE_API'] == 1 ? Api::User : Web::User

Fast (Rspec) tests with and without Rails

I have two classes:
1.Sale is a subclass of ActiveRecord; its job is to persist sales data to the database.
class Sale < ActiveRecord::Base
def self.total_for_duration(start_date, end_date)
self.count(conditions: {date: start_date..end_date})
end
#...
end
2.SalesReport is a standard Ruby class; its job is to produce and graph information about Sales.
class SalesReport
def initialize(start_date, end_date)
#start_date = start_date
#end_date = end_date
end
def sales_in_duration
Sale.total_for_duration(#start_date, #end_date)
end
#...
end
Because I want to use TDD and I want my tests to run really fast, I have written a spec for SalesReport that doesn't doesn't load Rails:
require_relative "../../app/models/sales_report.rb"
class Sale; end
# NOTE I have had to re-define Sale because I don't want to
# require `sale.rb` because it would then require ActiveRecord.
describe SalesReport do
describe "sales_in_duration" do
it "calls Sale.total_for_duration" do
Sale.should_receive(:total_for_duration)
SalesReport.new.sales_in_duration
end
end
end
This test works when I run bundle exec rspec spec/models/report_spec.rb.
However this test fails when I run bundle exec rake spec with the error superclass mismatch for class Sale (TypeError). I know the error is happening because Tap is defined by sale.rb and inline within the spec.
So my question is there a way to Stub (or Mock or Double) a class if that class isn't defined? This would allow me to remove the inline class Sale; end, which feels like a hack.
If not, how do I set up my tests such that they run correctly whether I run bundle exec rspec or bundle exec rake spec?
If not, is my approach to writing fast tests wrong?!
Finally, I don't want to use Spork. Thanks!
RSpec's recently added stub_const is specifically designed for cases like these:
describe SalesReport do
before { stub_const("Sale", Class.new) }
describe "sales_in_duration" do
it "calls Sale.total_for_duration" do
Sale.should_receive(:total_for_duration)
SalesReport.new.sales_in_duration
end
end
end
You may also want to use rspec-fire to use a test double in place of Sale that automatically checks all the mocked/stubbed methods exist on the real Sale class when running your tests with the real Sale class loaded (e.g. when you run your test suite):
require 'rspec/fire'
describe SalesReport do
include RSpec::Fire
describe "sales_in_duration" do
it "calls Sale.total_for_duration" do
fire_replaced_class_double("Sale")
Sale.should_receive(:total_for_duration)
SalesReport.new.sales_in_duration
end
end
end
If you rename total_for_duration on the real Sale class, rspec-fire will give you an error when you mock the method since it doesn't exist on the real class.
A simple way would be to check if "Sale" has already been defined
unless defined?(Sale)
class Sale; end
end
Sale need not be a class either in your test so:
unless defined?(Sale)
Sale = double('Sale')
end

Sinatra Set Settings (Ruby)

Using Sinatra in Ruby you can set the server's settings by doing:
set :myvariable, "MyValue"
and then access it anywhere in templates etc with settings.myvariable.
In my script I need to be able to re-set these variables falling back to a bunch of defaults. I figured the easiest way to do this would be to have a function that performs all the sets calling it at the start of the Sinatra server and when I need to make the alterations:
class MyApp < Sinatra::Application
helpers do
def set_settings
s = settings_from_yaml()
set :myvariable, s['MyVariable'] || "default"
end
end
# Here I would expect to be able to do:
set_settings()
# But the function isn't found!
get '/my_path' do
if things_go_right
set_settings
end
end
# Etc
end
As explained in the code above, the set_settings function isn't found, am I going about this the wrong way?
You're trying to call set_settings() inside the class scope of MyApp, but the helper method you used to define it only defines it for use inside that get... do...end block.
If you want set_settings() to be available statically (at class-load time instead of at request-process time), you need to define it as a class method:
class MyApp < Sinatra::Application
def self.set_settings
s = settings_from_yaml()
set :myvariable, s['MyVariable'] || "default"
end
set_settings
get '/my_path' do
# can't use set_settings here now b/c it's a class
# method, not a helper method. You can, however,
# do MyApp.set_settings, but the settings will already
# be set for this request.
end

Resources