Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

first commit

  • Loading branch information...
commit e8bd2c3364d1f1da8f48466abbec1dd260f8eba2 0 parents
@txus authored
4 .gitignore
@@ -0,0 +1,4 @@
+pkg/*
+*.gem
+.bundle
+*.rbc
2  .rspec
@@ -0,0 +1,2 @@
+--colour
+--format documentation
1  .rvmrc
@@ -0,0 +1 @@
+rvm --create use ruby-1.9.2@hijacker
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in hijacker.gemspec
+gemspec
48 Gemfile.lock
@@ -0,0 +1,48 @@
+PATH
+ remote: .
+ specs:
+ hijacker (0.0.1)
+ trollop
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ configuration (1.2.0)
+ diff-lcs (1.1.2)
+ guard (0.2.2)
+ open_gem (~> 1.4.2)
+ thor (~> 0.14.3)
+ guard-rspec (0.1.8)
+ guard (>= 0.2.0)
+ launchy (0.3.7)
+ configuration (>= 0.0.5)
+ rake (>= 0.8.1)
+ open_gem (1.4.2)
+ launchy (~> 0.3.5)
+ rake (0.8.7)
+ rspec (2.1.0)
+ rspec-core (~> 2.1.0)
+ rspec-expectations (~> 2.1.0)
+ rspec-mocks (~> 2.1.0)
+ rspec-core (2.1.0)
+ rspec-expectations (2.1.0)
+ diff-lcs (~> 1.1.2)
+ rspec-mocks (2.1.0)
+ simplecov (0.3.7)
+ simplecov-html (>= 0.3.7)
+ simplecov-html (0.3.9)
+ thor (0.14.4)
+ trollop (1.16.2)
+
+PLATFORMS
+ java
+ ruby
+
+DEPENDENCIES
+ bundler (~> 1.0.7)
+ guard
+ guard-rspec
+ hijacker!
+ rspec (~> 2.1.0)
+ simplecov
+ trollop
8 Guardfile
@@ -0,0 +1,8 @@
+# A sample Guardfile
+# More info at http://github.com/guard/guard#readme
+
+guard 'rspec', :version => 2 do
+ watch('^spec/(.*)_spec.rb')
+ watch('^lib/(.*)\.rb') { |m| "spec/#{m[1]}_spec.rb" }
+ watch('^spec/spec_helper.rb') { "spec" }
+end
10 Rakefile
@@ -0,0 +1,10 @@
+require 'bundler'
+Bundler::GemHelper.install_tasks
+
+require 'rspec/core'
+require 'rspec/core/rake_task'
+RSpec::Core::RakeTask.new(:spec) do |spec|
+ spec.pattern = FileList['spec/**/*_spec.rb']
+end
+
+task :default => :spec
114 Readme.md
@@ -0,0 +1,114 @@
+#hijacker
+
+A little gem that hijacks any ruby object and broadcasts all its activity
+to a particular hijacker server. Useful for logging and those awfully hardcore
+debugging afternoons! There might be other uses to it, for sure. Just be
+creative :)
+
+Hijacker is tested with Ruby 1.8.7, 1.9.2, JRuby 1.5.3 and Rubinius 1.1.
+
+##Install and configure
+
+In your Gemfile:
+
+ gem 'hijacker'
+
+If you are using Rails, you might want to put this configuration snippet in an
+initializer or something (you can always put it in any other part of the code
+otherwise, as long as it's before hijacking any object):
+
+ Hijacker.configure do
+ uri '<YOUR HIJACKER SERVER URI>'
+ end
+
+And that's it! Oooor not. You have to spawn your server. In the command line:
+
+ hijacker
+
+And it will output the URI for this server. *Note this* and pass it to your
+configuration block!
+
+Some options you can pass to the server command:
+
+ hijacker --without-timestamps (don't show the timestamps)
+ hijacker --without-classes (don't show the object classes)
+ hijacker --port 1234 (spawn the server in port 1234 rather than 8787)
+
+##Ok, and now for some hijacking action!
+
+ require 'hijacker' # You don't have to when using Bundler :)
+
+ class MyClass
+ def foo(bar, baz)
+ bar + baz
+ end
+ end
+
+ some_object = Object.new
+
+ # These are the important lines:
+
+ Hijacker.spy(some_object)
+ Hijacker.spy(MyClass)
+
+ instance = MyClass.new
+ instance.foo(3, 4)
+
+Run this code and, if you look at the server output, you'll see nothing less
+than...
+
+ <a nice timestamp> MyClass (Class) received :new and returned #<MyClass:0x000000874> (MyClass)
+ <a nice timestamp> #<MyClass:0x000000874> (MyClass) received :foo with 3
+ (Fixnum), 4 (Fixnum) and returned 7
+
+If you want to un-hijack any object, just call #restore:
+
+ Hijacker.restore(MyClass)
+ Hijacker.restore(some_object)
+
+If you don't want to have to remember every hijacked object you have to call
+restore on it, you can just spy a particular object within the duration of a block:
+
+ Hijacker.spying(MyClass) do
+ # inside this block, MyClass will be spied
+ end
+ # here not anymore
+
+Awesome! You can fine-tune your spying, for example by only spying on instance
+methods or singleton methods only:
+
+ Hijacker.spy(MyClass, :only => :instance_methods) # or :singleton_methods
+
+And, last but not least... you can specify a *particular hijacker server* for
+a *particular object* you are spying on!
+
+ # All activity on MyClass and its instances will
+ # be sent to druby://localhost:9999
+ Hijacker.spy(MyClass, :uri => 'druby://localhost:9999')
+
+ # But for example, the activity of some_object will
+ # be sent to the default uri specified in the configuration
+ # back earlier (remember?)
+ Hijacker.spy(some_object)
+
+Of course, you can specify a particular server uri for a block, with #spying:
+
+ Hijacker.spying(foo_object, :uri => 'druby://localhost:1234') do
+ # all the activity of foo_object inside this block
+ # will be sent to the hijacker server on druby://localhost:1234
+ end
+
+##Note on Patches/Pull Requests
+
+* Fork the project.
+* Make your feature addition or bug fix.
+* Add specs for it. This is important so I don't break it in a
+ future version unintentionally.
+* Commit, do not mess with rakefile, version, or history.
+ If you want to have your own version, that is fine but bump version
+ in a commit by itself I can ignore when I pull.
+* Send me a pull request. Bonus points for topic branches.
+
+## Copyright
+
+Copyright (c) 2010 Josep M. Bach. See LICENSE for details.
50 bin/hijacker
@@ -0,0 +1,50 @@
+#!/usr/bin/env ruby -w
+# hijacker server
+$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
+
+require 'hijacker'
+
+opts = Trollop::options do
+ version "hijacker 0.0.1 (c) 2010 Josep M. Bach"
+ banner <<-EOS
+ Hijacker server listens for reports by hijackers spying on ruby objects.
+
+ Usage:
+ hijacker [options]
+ where [options] are:
+EOS
+ opt :without_classes, "Don't show classes of objects"
+ opt :without_timestamps, "Don't show timestamps"
+ opt :port, "DRb port to use (default is 8787)", :default => 8787
+end
+
+begin
+ raise unless opts[:port].to_i > 0 && opts[:port].to_i < 9999
+rescue
+ Trollop::die :port, "must be a valid number between 0 and 9999"
+end
+
+DRB_URI="druby://localhost:#{opts[:port]}"
+
+# Start up the DRb service
+DRb.start_service DRB_URI, Hijacker::Logger.new(opts)
+
+ANSI = Hijacker::Logger::ANSI
+# We need the uri of the service to connect a client
+welcome = []
+welcome << ANSI[:BOLD] + "hijacker server"
+welcome << "listening on"
+welcome << ANSI[:BOLD] + DRb.uri + ANSI[:RESET]
+puts welcome.join("#{ANSI[:RESET]} ") + "\n"
+
+# We need the uri of the service to connect a client
+instructions = "Put this code in the configuration of your ruby program #{ANSI[:BOLD]}before any call to Hijacker#{ANSI[:RESET]}:\n\n"
+instructions += "\t" + "Hijacker.config do\n"
+instructions += "\t" + " uri '#{DRb.uri}'\n"
+instructions += "\t" + "end\n\n"
+puts instructions
+instructions = "Or optionally attach a particular hijacked object to this server adding :uri => '#{DRb.uri}' when calling Hijacker's :spy or :spying method.\n\n"
+puts instructions
+
+# wait for the DRb service to finish before exiting
+DRb.thread.join
30 hijacker.gemspec
@@ -0,0 +1,30 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "hijacker/version"
+
+Gem::Specification.new do |s|
+ s.name = "hijacker"
+ s.version = Hijacker::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["Josep M. Bach"]
+ s.email = ["josep.m.bach@gmail.com"]
+ s.homepage = "http://github.com/txus/hijacker"
+ s.summary = %q{Spy on your ruby objects and send their activity to a hijacker server anywhere through DRb}
+ s.description = %q{Spy on your ruby objects and send their activity to a hijacker server anywhere through DRb}
+
+ s.rubyforge_project = "hijacker"
+
+ s.add_runtime_dependency 'trollop'
+ s.default_executable = "hijacker"
+
+ s.add_development_dependency 'bundler', '~> 1.0.7'
+ s.add_development_dependency 'rspec', '~> 2.1.0'
+ s.add_development_dependency 'guard'
+ s.add_development_dependency 'guard-rspec'
+ s.add_development_dependency "simplecov"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+end
116 lib/hijacker.rb
@@ -0,0 +1,116 @@
+require 'drb'
+require 'trollop'
+require 'hijacker/config'
+require 'hijacker/logger'
+
+module Hijacker
+
+ # Methods that won't be hijacked in any case
+ REJECTED_METHODS = (Object.instance_methods | Module.methods | %w{< <= > >= __original_[\w\d]+ [^\w\d]+})
+ FORBIDDEN_CLASSES = [Array, Hash, String, Fixnum, Float, Numeric, Symbol, Proc, Class, Object, BasicObject, Module]
+
+ class << self
+
+ def spying(*args, &block)
+ raise "No block given" unless block
+ Hijacker.spy(*args)
+ block.call
+ Hijacker.restore(args.first)
+ end
+
+ def spy(object, options = {})
+ raise "Cannot spy on the following forbidden classes: #{FORBIDDEN_CLASSES.map(&:to_s).join(', ')}" if FORBIDDEN_CLASSES.include?(object)
+ rejection = /^(#{REJECTED_METHODS.join('|')})/
+ only = options[:only]
+ uri = options[:uri]
+ custom_rejection = options[:reject] if options[:reject].is_a?(Regexp)
+
+ inst_methods = guess_instance_methods_from(object).reject{|m| (m =~ rejection)}.reject{|m| m =~ custom_rejection}
+ sing_methods = guess_class_methods_from(object).reject{|m| m =~ rejection}.reject{|m| m =~ custom_rejection}
+
+ receiver = if object.is_a?(Class)
+ object
+ else
+ (class << object; self; end)
+ end
+
+ inst_methods.each do |met|
+ receiver.send(:alias_method, :"__original_#{met}", :"#{met}")
+ receiver.send(:undef_method, :"#{met}")
+ receiver.class_eval <<EOS
+ def #{met}(*args, &blk)
+ __original_#{met}(*args,&blk).tap do |retval|
+ Hijacker.register :#{met}, args, retval, self.dup, #{uri.inspect}
+ end
+ end
+EOS
+ end unless options[:only] == :singleton_methods
+
+ receiver = (class << object; self; end)
+ sing_methods.each do |met|
+ puts "Defining #{met}"
+ receiver.send(:alias_method, :"__original_#{met}", :"#{met}")
+ receiver.send(:undef_method, :"#{met}")
+ receiver.class_eval <<EOS
+ def #{met}(*args, &blk)
+ __original_#{met}(*args,&blk).tap do |retval|
+ Hijacker.register :#{met}, args, retval, self.dup, #{uri.inspect}
+ end
+ end
+EOS
+ end unless options[:only] == :instance_methods
+
+ end
+
+ def restore(object)
+ receiver = if object.is_a?(Class)
+ object
+ else
+ (class << object; self; end)
+ end
+ guess_instance_methods_from(object).select{|m| m =~ /__original_/}.each do |met|
+ met = met.to_s.gsub!("__original_", "")
+ receiver.send(:undef_method, :"#{met}")
+ receiver.send(:alias_method, :"#{met}", :"__original_#{met}")
+ end
+
+ receiver = (class << object; self; end)
+ guess_class_methods_from(object).select{|m| m =~ /__original_/}.each do |met|
+ met = met.to_s.gsub!("__original_", "")
+ receiver.send(:undef_method, :"#{met}")
+ receiver.send(:alias_method, :"#{met}", :"__original_#{met}")
+ end
+ end
+
+ def register(method, args, retval, object, uri = nil)
+ args.map! do |arg|
+ {:inspect => arg.inspect, :class => arg.class.name}
+ end
+ retval = {:inspect => retval.inspect, :class => retval.class.name}
+ object = {:inspect => object.inspect, :class => object.class.name}
+
+ server = DRbObject.new nil, (uri || self.drb_uri)
+ server.log method, args, retval, object
+ end
+
+ private
+
+ def guess_instance_methods_from(object)
+ if object.is_a?(Class)
+ object.instance_methods
+ else
+ object.methods
+ end
+ end
+
+ def guess_class_methods_from(object)
+ if object.is_a?(Class)
+ object.methods
+ else
+ []
+ end
+ end
+
+ end
+
+end
19 lib/hijacker/config.rb
@@ -0,0 +1,19 @@
+module Hijacker
+ class << self
+ def configure(&block)
+ self.instance_eval(&block)
+ end
+
+ def uri(drb)
+ @@drb_uri = drb
+ end
+
+ def drb_uri
+ begin
+ @@drb_uri
+ rescue
+ raise "Neither a global nor a local Hijacker server URI is configured. Please refer to the README to find out how to do this."
+ end
+ end
+ end
+end
53 lib/hijacker/logger.rb
@@ -0,0 +1,53 @@
+module Hijacker
+ class Logger
+
+ ANSI = {:RESET=>"\e[0m", :BOLD=>"\e[1m", :UNDERLINE=>"\e[4m",
+ :LGRAY=>"\e[0;37m", :GRAY=>"\e[1;30m",
+ :RED=>"\e[31m",
+ :GREEN=>"\e[32m", :LGREEN=>"\e[1;32m",
+ :YELLOW=>"\e[33m",
+ :BLUE=>"\e[34m", :LBLUE=>"\e[1;34m",
+ :PURPLE=>"\e[35m", :LPURPLE=>"\e[1;35m",
+ :CYAN=>"\e[36m", :LCYAN=>"\e[1;36m",
+ :WHITE=>"\e[37m"}
+
+ # Make dRuby send Logger instances as dRuby references,
+ # not copies.
+ include DRb::DRbUndumped
+
+ attr_reader :opts
+
+ def initialize(opts)
+ @opts = opts
+ end
+
+ def log(method, args, retval, object)
+ out = []
+ out << ANSI[:BOLD] + ANSI[:UNDERLINE] + "#{Time.now}" unless opts[:without_timestamps]
+ out << ANSI[:CYAN] + object[:inspect]
+ out << ANSI[:LCYAN] + "(#{object[:class]})" unless opts[:without_classes]
+ out << "received"
+ out << ANSI[:RED] + ":#{method}"
+ unless args.empty?
+ out << "with"
+ out << args.map do |arg|
+ ANSI[:GREEN] + arg[:inspect] + ANSI[:LGREEN] + (opts[:without_classes] ? "" : " (#{arg[:class]})") +
+ ANSI[:RESET]
+ end.join(', ')
+ end
+ out << "and returned"
+ out << ANSI[:BLUE] + retval[:inspect]
+ out << ANSI[:LBLUE] + "(#{retval[:class]})" unless opts[:without_classes]
+ out << ANSI[:RESET] + "\n"
+ stdout.print out.join("#{ANSI[:RESET]} ")
+ end
+
+ private
+
+ def stdout
+ $stdout
+ end
+
+ end
+
+end
3  lib/hijacker/version.rb
@@ -0,0 +1,3 @@
+module Hijacker
+ VERSION = "0.0.1"
+end
14 spec/hijacker/config_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe Hijacker, "configuration" do
+
+ describe "#configure" do
+ it 'accepts a block with the \'uri\' configuration option' do
+ Hijacker.configure do
+ uri 'druby://localhost:8787'
+ end
+ Hijacker.drb_uri.should == 'druby://localhost:8787'
+ end
+ end
+
+end
81 spec/hijacker/logger_spec.rb
@@ -0,0 +1,81 @@
+require 'spec_helper'
+
+module Hijacker
+ describe Logger do
+
+ subject { Logger.new({:my => :option}) }
+
+ it "initializes with options" do
+ subject.opts.should == {:my => :option}
+ end
+
+ describe "#log" do
+
+ let(:args) do
+ [:bar,
+ [
+ {:inspect => "2", :class => "Fixnum"},
+ {:inspect => "\"string\"", :class => "String"},
+ ],
+ {:inspect => "\"retval\"", :class => "String"},
+ {:inspect => "MyClass", :class => "Class"}]
+ end
+
+ it 'prints the received args' do
+ out = StringIO.new
+ subject.stub(:stdout).and_return out
+
+ Time.stub(:now).and_return Time.parse('2010-11-20')
+
+ subject.log(*args)
+
+ ["00:00:00 +0100",
+ "MyClass",
+ "(Class)",
+ "received",
+ ":bar",
+ "with",
+ "2",
+ "(Fixnum)",
+ "\"string\"",
+ "(String)",
+ "and returned",
+ "\"retval\"",
+ "(String)"].each do |str|
+ out.string.should include(str)
+ end
+ end
+ context "when given :without_timestamps" do
+ it 'discards the timestamps' do
+ logger = Logger.new({:without_timestamps => true})
+
+ out = StringIO.new
+ logger.stub(:stdout).and_return out
+
+ Time.stub(:now).and_return Time.parse('2010-11-20')
+
+ logger.log(*args)
+
+ out.string.should_not include("2010-11-20")
+ end
+ end
+ context "when given :without_classes" do
+ it 'discards the classes' do
+ logger = Logger.new({:without_classes => true})
+
+ out = StringIO.new
+ logger.stub(:stdout).and_return out
+
+ Time.stub(:now).and_return Time.parse('2010-11-20')
+
+ logger.log(*args)
+
+ ["(Class)", "(Fixnum)", "(String)"].each do |str|
+ out.string.should_not include(str)
+ end
+ end
+ end
+ end
+
+ end
+end
152 spec/hijacker_spec.rb
@@ -0,0 +1,152 @@
+require 'spec_helper'
+
+class MyClass
+ def self.foo
+ 3 + 4
+ end
+ def self.bar(a,b)
+ b
+ end
+
+ def foo
+ 3 + 4
+ end
+ def bar(a,b)
+ b
+ end
+end
+
+describe Hijacker do
+
+ describe "#spying" do
+ it 'runs a block spying on a particular object' do
+ blk = lambda {
+ MyClass.foo
+ }
+ Hijacker.should_receive(:spy).with(MyClass).once.ordered
+ MyClass.should_receive(:foo).once.ordered
+ Hijacker.should_receive(:restore).with(MyClass).once.ordered
+
+ Hijacker.spying(MyClass, &blk)
+ end
+
+ it 'raises if no block given' do
+ expect {
+ Hijacker.spying(MyClass)
+ }.to raise_error("No block given")
+ end
+ end
+
+ describe "#spy - #restore" do
+
+ describe "hijacking a Class" do
+ describe "instance methods" do
+ before(:each) do
+ Hijacker.spy(MyClass, :only => :instance_methods)
+ end
+ it "registers method calls without arguments" do
+ Hijacker.should_receive(:register).with(:foo, [], 7, kind_of(MyClass), nil).ordered
+ MyClass.new.foo.should == 7
+ end
+ it "registers method calls with arguments" do
+ Hijacker.should_receive(:register).with(:bar, [2, "string"], "string", kind_of(MyClass), nil).ordered
+ MyClass.new.bar(2, "string").should == "string"
+ end
+ after(:each) do
+ Hijacker.restore(MyClass)
+ end
+ end
+ describe "class methods" do
+ before(:each) do
+ Hijacker.spy(MyClass)
+ end
+ it "registers method calls without arguments" do
+ Hijacker.should_receive(:register).with(:foo, [], 7, kind_of(Class), nil).ordered
+ MyClass.foo.should == 7
+ end
+ it "registers method calls with arguments" do
+ Hijacker.should_receive(:register).with(:bar, [2, "string"], "string", kind_of(Class), nil).ordered
+ MyClass.bar(2, "string").should == "string"
+ end
+ after(:each) do
+ Hijacker.restore(MyClass)
+ end
+ end
+ describe "forbidden classes (are not hijacked)" do
+ [Array, Hash, String, Fixnum, Float, Numeric, Symbol].each do |forbidden|
+ it "protects #{forbidden}" do
+ expect {
+ Hijacker.spy(forbidden)
+ }.to raise_error
+ end
+ end
+ end
+ end
+ describe "hijacking an object" do
+ describe "instance methods" do
+ let(:object) { MyClass.new }
+
+ before(:each) do
+ def object.my_method
+ 8
+ end
+ def object.my_method_with_args(a,b)
+ b
+ end
+ Hijacker.spy(object)
+ end
+ it "registers method calls without arguments" do
+ Hijacker.should_receive(:register).with(:foo, [], 7, kind_of(MyClass), nil).ordered
+ Hijacker.should_receive(:register).with(:my_method, [], 8, kind_of(MyClass), nil).ordered
+
+ object.foo.should == 7
+ object.my_method.should == 8
+ end
+ it "registers method calls with arguments" do
+ Hijacker.should_receive(:register).with(:bar, [2, "string"], "string", kind_of(MyClass), nil).ordered
+ Hijacker.should_receive(:register).with(:my_method_with_args, [2, "string"], "string", kind_of(MyClass), nil).ordered
+
+ object.bar(2, "string").should == "string"
+ object.my_method_with_args(2, "string").should == "string"
+ end
+ it "does not affect other instances of the object's class" do
+ Hijacker.should_not_receive(:register)
+ MyClass.new.foo.should == 7
+ end
+ after(:each) do
+ Hijacker.restore(object)
+ end
+ end
+ end
+
+ end
+
+ describe "#register" do
+ it 'sends the registered method call to the DRb server' do
+ server = mock('DRb server')
+
+ Hijacker.stub(:drb_uri).and_return "druby://localhost:9999"
+
+ expected_args = [:bar,
+ [
+ {:inspect => "2", :class => "Fixnum"},
+ {:inspect => "\"string\"", :class => "String"},
+ ],
+ {:inspect => "\"retval\"", :class => "String"},
+ {:inspect => "MyClass", :class => "Class"}
+ ]
+
+ DRbObject.should_receive(:new).with(nil, "druby://localhost:9999").and_return server
+ server.should_receive(:log).with *expected_args
+
+ Hijacker.register(:bar, [2, "string"], "retval", MyClass)
+ end
+ context "when given a particular DRb uri" do
+ it "sends the call to that uri" do
+ DRbObject.should_receive(:new).with(nil, "druby://localhost:1212").and_return mock('DRb server', :log => true)
+ Hijacker.register(:bar, [2, "string"], "retval", MyClass, "druby://localhost:1212")
+ end
+ end
+ end
+
+end
17 spec/spec_helper.rb
@@ -0,0 +1,17 @@
+require 'bundler'
+begin
+ Bundler.setup(:default, :development)
+rescue Bundler::BundlerError => e
+ $stderr.puts e.message
+ $stderr.puts "Run `bundle install` to install missing gems"
+ exit e.status_code
+end
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+require 'hijacker'
+require 'rspec'
+
+# Requires supporting files with custom matchers and macros, etc,
+# in ./support/ and its subdirectories.
+Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
Please sign in to comment.
Something went wrong with that request. Please try again.