How to extract tasks and variables from a Rakefile? - ruby

I need to:
Open a Rakefile
Find if a certain task is defined
Find if a certain variable is defined
This works to find tasks defined inside a Rakefile, but it pollutes the global namespace (i.e. if you run it twice, all tasks defined in first one will show up in the second one):
sub_rake = Rake::DefaultLoader.new
sub_rake.load("Rakefile")
puts Rake.application.tasks
In Rake, here is where it loads the Makefile:
https://github.com/ruby/rake/blob/master/lib/rake/rake_module.rb#L28
How do I get access to the variables that are loaded there?
Here is an example Rakefile I am parsing:
load '../common.rake'
#source_dir = 'source'
desc "Run all build and deployment tasks, for continuous delivery"
task :deliver => ['git:pull', 'jekyll:build', 'rsync:push']
Here's some things I tried that didn't work. Using eval on the Rakefile:
safe_object = Object.new
safe_object.instance_eval("Dir.chdir('" + f + "')\n" + File.read(folder_rakefile))
if safe_object.instance_variable_defined?("#staging_dir")
puts " Staging directory is " + f.yellow + safe_object.instance_variable_get("#staging_dir").yellow
else
puts " Staging directory is not specified".red
end
This failed when parsing desc parts of the Rakefile. I also tried things like
puts Rake.instance_variables
puts Rake.class_variables
But these are not getting the #source_dir that I am looking for.

rakefile_body = <<-RUBY
load '../common.rake'
#source_dir = 'some/source/dir'
desc "Run all build and deployment tasks, for continuous delivery"
task :deliver => ['git:pull', 'jekyll:build', 'rsync:push']
RUBY
def source_dir(ast)
return nil unless ast.kind_of? AST::Node
if ast.type == :ivasgn && ast.children[0] == :#source_dir
rhs = ast.children[1]
if rhs.type != :str
raise "#source_dir is not a string literal! #{rhs.inspect}"
else
return rhs.children[0]
end
end
ast.children.each do |child|
value = source_dir(child)
return value if value
end
nil
end
require 'parser/ruby22'
body = Parser::Ruby22.parse(rakefile_body)
source_dir body # => "some/source/dir"

Rake runs load() on the Rakefile inside load_rakefile in the Rake module. And you can easily get the tasks with the public API.
Rake.load_rakefile("Rakefile")
puts Rake.application.tasks
Apparently that load() invocation causes the loaded variables to be captured into the main Object. This is the top-level Object of Ruby. (I expected it to be captured into Rake since the load call is made in the context of the Rake module.)
Therefore, it is possible to access instance variables from the main object using this ugly code:
main = eval 'self', TOPLEVEL_BINDING
puts main.instance_variable_get('#staging_dir')
Here is a way to encapsulate the parsing of the Rakefile so that opening two files will not have all the things from the first one show up when you are analyzing the second one:
class RakeBrowser
attr_reader :tasks
attr_reader :variables
include Rake::DSL
def task(*args, &block)
if args.first.respond_to?(:id2name)
#tasks << args.first.id2name
elsif args.first.keys.first.respond_to?(:id2name)
#tasks << args.first.keys.first.id2name
end
end
def initialize(file)
#tasks = []
Dir.chdir(File.dirname(file)) do
eval(File.read(File.basename(file)))
end
#variables = Hash.new
instance_variables.each do |name|
#variables[name] = instance_variable_get(name)
end
end
end
browser = RakeBrowser.new(f + "Rakefile")
puts browser.tasks
puts browser.variables[:#staging_dir]

Related

Capybara.page not in scope after extending capybara-screenshot's after_failed_example method

I'm trying to override the after_failed_example method so I can inflict some custom file naming on our screenshots. I'm loading the module as an initializer.
So far, so good, but the Capybara.page.current_url is blank, making me think I need to require something additional?
require "capybara-screenshot/rspec"
module Capybara
module Screenshot
module RSpec
class << self
attr_accessor :use_description_as_filename
attr_accessor :save_html_file
end
self.use_description_as_filename = true
self.save_html_file = true
def self.after_failed_example(example)
if example.example_group.include?(Capybara::DSL) # Capybara DSL method has been included for a feature we can snapshot
Capybara.using_session(Capybara::Screenshot.final_session_name) do
puts ">>>> Capybara.page.current_url: " + Capybara.page.current_url.to_s
if Capybara::Screenshot.autosave_on_failure && failed?(example) && Capybara.page.current_url != ''
saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, Capybara::Screenshot.save_html_file?, set_saver_filename_prefix(example))
saver.save
example.metadata[:screenshot] = {}
example.metadata[:screenshot][:html] = saver.html_path if saver.html_saved?
example.metadata[:screenshot][:image] = saver.screenshot_path if saver.screenshot_saved?
end
end
end
private
def self.set_saver_filename_prefix(example)
return example.description.to_s.gsub(" ", "-") if Capybara::Screenshot.use_description_as_filename?
return Capybara::Screenshot.filename_prefix_for(:rspec, example)
end
end
end
end
end
This is successfully overriding the capybara-screenshot/rspec method, and any of the Capybara::Screenshot static information is accessible, but not Capybara session related information (afa I can tell).
For example, Capybara.page.current_url.to_s is null when overridden, but present when not.
I was missing a require (kind of silly mistake):
require 'capybara/rspec'

how to share a rake variable in the code it invokes?

What I need is basically send a target argument and use it in my RSpec tests, e.g.:
$ rake unistall_and_run[test_spec.rb]
Rakefile:
desc 'uninstall app to run tests'
task :uninstall_and_run, [:arg] do |t, arg|
#note this, i will explain later
start_driver(fullReset: true)
oi = arg.to_s.split('"')[1]
file_dir = (project_home + '/spec/' + oi)
exec "rspec #{file_dir}"
end
start_driver is called on that line, but when I run the tests (exec "rspec ..."), it is called again and the args I passed is overwritten by the default (because its on RSpec config).
What I'd like to do is, on my RSpec file check if it was already called and don't run again.
Here is the start_driver method:
def start_driver(options= {})
if options.empty?
capabilities = caps
else
capabilities = caps(options)
end
$appium = Appium::Driver.new(caps: capabilities)
$browser = $appium.start_driver
Appium.promote_appium_methods RSpec::Core::ExampleGroup
end
So, i found a way to do it. Its not beautiful though. I save a file with the args I want when run rake:
desc 'uninstall app to run tests'
task :uninstall_and_run, [:arg] do |t, arg|
send_custom_caps(fullReset: true)
oi = arg.to_s.split('"')[1]
file_dir = (project_home + '/spec/' + oi)
exec "rspec #{file_dir}"
end
and the send_custom_caps method is:
def send_custom_caps(*opts)
file = File.new(custom_caps_file, "w+")
File.open(file, 'w') do |f|
f.write(opts)
end
end
now the ruby code itself (in this case, my spec config) will check if there is custom args before start_driver. Here is my custom start_driver method (which I renamed):
def start_appium_driver (options= {})
if options.empty?
get_caps
if $custom_args
capabilities = caps($custom_args)
else
capabilities = caps
end
else
capabilities = caps(options)
end
$appium = Appium::Driver.new(caps: capabilities)
$browser = $appium.start_driver
Appium.promote_appium_methods RSpec::Core::ExampleGroup
end
and get_caps
def get_caps
if File.exist?(custom_caps_file) #$custom_args
file = File.read(custom_caps_file)
$custom_args = eval(file)
File.delete (custom_caps_file)
end
$custom_args unless $custom_args.defined?
end
probably this is not the best solution, but it is working ok for me :)

Override rake test:units runner

I recently decided to write a simple test runtime profiler for our Rails 3.0 app's test suite. It's a very simple (read: hacky) script that adds each test's time to a global, and then outputs the result at the end of the run:
require 'test/unit/ui/console/testrunner'
module ProfilingHelper
def self.included mod
$test_times ||= []
mod.class_eval do
setup :setup_profiling
def setup_profiling
#test_start_time = Time.now
end
teardown :teardown_profiling
def teardown_profiling
#test_took_time = Time.now - #test_start_time
$test_times << [name, #test_took_time]
end
end
end
end
class ProfilingRunner < Test::Unit::UI::Console::TestRunner
def finished(elapsed_time)
super
tests = $test_times.sort{|x,y| y[1] <=> x[1]}.first(100)
output("Top 100 slowest tests:")
tests.each do |t|
output("#{t[1].round(2)}s: \t #{t[0]}")
end
end
end
Test::Unit::AutoRunner::RUNNERS[:profiling] = proc do |r|
ProfilingRunner
end
This allows me to run the suites like so rake test:xxx TESTOPTS="--runner=profiling" and get a list of Top 100 tests appended to the end of the default runner's output. It works great for test:functionals and test:integration, and even for test:units TEST='test/unit/an_example_test.rb'. But if I do not specify a test for test:units, the TESTOPTS appears to be ignored.
In classic SO style, I found the answer after articulating clearly to myself, so here it is:
When run without TEST=/test/unit/blah_test.rb, test:units TESTOPTS= needs a -- before its contents. So the solution in its entirety is simply:
rake test:units TESTOPTS='-- --runner=profiling'

How to get my custom Rake task *args to work like built-in Rake task *args?

I have a custom rake task, implemented as a method, which sits "above" the task method. It works with a custom configuration object to give the whole thing a more declarative feel. Here it is.
def robocopy(*args, &block)
config = RobocopyConfiguration.new
block.call config
body = proc {
system "robocopy #{config.make_parameters.join ' '}"
}
Rake::Task.define_task *args, &body
end
class RobocopyConfiguration
attr_accessor :source, :destination, :files
attr_reader :mirror
def mirror
#mirror = true
end
def make_parameters
parameters = [ #source, #destination ]
parameters << #files.flatten if #files
parameters << '/MIR' if #mirror
parameters
end
end
And you can declare it like a normal-ish rake task with a name and dependencies.
robocopy :copy => [ :build ] do |cmd|
cmd.source = File.expand_path File.dirname __FILE__
cmd.destination = '//some/default/network/share'
cmd.mirror
end
However, as soon as you add arguments, things start to blow up.
robocopy :copy, [ :destination ] => [ :build ] do |cmd, args|
args.with_defaults(:destination => '//some/default/network/share')
cmd.source = File.expand_path File.dirname __FILE__
cmd.destination = args[:destination]
cmd.mirror
end
cmd> rake copy['//some/network/share']
rake aborted!
undefined method `with_defaults' for nil:NilClass
I suspect the *args aren't getting turned into TaskArguments, with all the special methods, they're are being used immediately in the custom block.call. And I can't quite figure out what call to make to turn them into the right kind of arguments.

Creating a common method for Capistrano tasks

Let's say in my standard deploy.rb file I have a set of namespaces. I have a common task that lists RPM packages based on a variable I pass to it. When I run this as is, it complains about capture being an undefined method. If I include that method inside the deploy.rb file, it works just fine.
Mind you, I'm new to ruby and to OOP so I'm sure I'm doing this the wrong way. :-)
deploy.rb
load 'config/module'
namespace :lp_app do
desc "LP tasks"
co = Tasks::Commands.new()
task :list do
co.list_pkg("LP")
end
end
module.rb
module Tasks
class Commands
def list_pkg(component)
File.open("#{component}.file.list", "r").each_line do |line|
pkg_name = "#{line}".chomp
set :server_pkg, capture("rpm -q #{pkg_name}")
puts "#{server_pkg}"
end
end
end
end
You are trying to use Capistano specific commands outside of Capistrano. If you want to set a variable to the result of something you run on the command line, try the backtick (`).
module Tasks
class Commands
def list_pkg(component)
File.open("#{component}.file.list", "r").each_line do |line|
pkg_name = "#{line}".chomp
server_pkg = `rpm -q #{pkg_name}`
puts server_pkg
end
end
end
end

Resources