diff --git a/Gemfile b/Gemfile
index 042c3437bb..689efb67fd 100755
--- a/Gemfile
+++ b/Gemfile
@@ -51,11 +51,10 @@ group :test do
gem 'database_cleaner'
# testing framework
gem 'rspec', '>= 2.12'
- # add matchers from shoulda, such as query_the_database, which is useful for
- # testing that the Msf::DBManager activation is respected.
gem 'shoulda-matchers'
# code coverage for tests
# any version newer than 0.5.4 gives an Encoding error when trying to read the source files.
+ # see: https://github.com/colszowka/simplecov/issues/127 (hopefully fixed in 0.8.0)
gem 'simplecov', '0.5.4', :require => false
# Manipulate Time.now in specs
gem 'timecop'
diff --git a/Gemfile.lock b/Gemfile.lock
index c532448b29..fd3a6ad609 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -13,23 +13,19 @@ GEM
i18n (= 0.6.1)
multi_json (~> 1.0)
arel (3.0.2)
- bourne (1.4.0)
- mocha (~> 0.13.2)
builder (3.0.4)
- database_cleaner (0.9.1)
- diff-lcs (1.2.2)
+ database_cleaner (1.1.1)
+ diff-lcs (1.2.4)
factory_girl (4.2.0)
activesupport (>= 3.0.0)
- i18n (0.6.1)
- json (1.7.7)
- metaclass (0.0.1)
+ i18n (0.6.5)
+ json (1.8.0)
metasploit_data_models (0.16.6)
activerecord (>= 3.2.13)
activesupport
pg
- mocha (0.13.3)
- metaclass (~> 0.0.1)
- msgpack (0.5.4)
+ mini_portile (0.5.1)
+ msgpack (0.5.5)
multi_json (1.0.4)
network_interface (0.0.1)
nokogiri (1.5.9)
@@ -39,22 +35,21 @@ GEM
rake (10.0.4)
redcarpet (2.2.2)
robots (0.10.1)
- rspec (2.13.0)
- rspec-core (~> 2.13.0)
- rspec-expectations (~> 2.13.0)
- rspec-mocks (~> 2.13.0)
- rspec-core (2.13.1)
- rspec-expectations (2.13.0)
+ rspec (2.14.1)
+ rspec-core (~> 2.14.0)
+ rspec-expectations (~> 2.14.0)
+ rspec-mocks (~> 2.14.0)
+ rspec-core (2.14.5)
+ rspec-expectations (2.14.2)
diff-lcs (>= 1.1.3, < 2.0)
- rspec-mocks (2.13.0)
- shoulda-matchers (1.5.2)
+ rspec-mocks (2.14.3)
+ shoulda-matchers (2.3.0)
activesupport (>= 3.0.0)
- bourne (~> 1.3)
simplecov (0.5.4)
multi_json (~> 1.0.3)
simplecov-html (~> 0.5.3)
simplecov-html (0.5.3)
- timecop (0.6.1)
+ timecop (0.6.3)
tzinfo (0.3.37)
yard (0.8.5.2)
diff --git a/spec/support/matchers/query_the_database.rb b/spec/support/matchers/query_the_database.rb
new file mode 100644
index 0000000000..c7089c3ac9
--- /dev/null
+++ b/spec/support/matchers/query_the_database.rb
@@ -0,0 +1,108 @@
+module Shoulda # :nodoc:
+ module Matchers
+ module ActiveRecord # :nodoc:
+
+ # Ensures that the number of database queries is known. Rails 3.1 or greater is required.
+ #
+ # Options:
+ # * when_calling - Required, the name of the method to examine.
+ # * with - Used in conjunction with when_calling to pass parameters to the method to examine.
+ # * or_less - Pass if the database is queried no more than the number of times specified, as opposed to exactly that number of times.
+ #
+ # Examples:
+ # it { should query_the_database(4.times).when_calling(:complicated_counting_method)
+ # it { should query_the_database(4.times).or_less.when_calling(:generate_big_report)
+ # it { should_not query_the_database.when_calling(:cached_count)
+ #
+ def query_the_database(times = nil)
+ QueryTheDatabaseMatcher.new(times)
+ end
+
+ class QueryTheDatabaseMatcher # :nodoc:
+ def initialize(times)
+ @queries = []
+ @options = {}
+
+ if times.respond_to?(:count)
+ @options[:expected_query_count] = times.count
+ else
+ @options[:expected_query_count] = times
+ end
+ end
+
+ def when_calling(method_name)
+ @options[:method_name] = method_name
+ self
+ end
+
+ def with(*method_arguments)
+ @options[:method_arguments] = method_arguments
+ self
+ end
+
+ def or_less
+ @options[:expected_count_is_maximum] = true
+ self
+ end
+
+ def matches?(subject)
+ subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |name, started, finished, id, payload|
+ @queries << payload unless filter_query(payload)
+ end
+
+ if @options[:method_arguments]
+ subject.send(@options[:method_name], *@options[:method_arguments])
+ else
+ subject.send(@options[:method_name])
+ end
+
+ ActiveSupport::Notifications.unsubscribe(subscriber)
+
+ if @options[:expected_count_is_maximum]
+ @queries.length <= @options[:expected_query_count]
+ elsif @options[:expected_query_count].present?
+ @queries.length == @options[:expected_query_count]
+ else
+ @queries.length > 0
+ end
+ end
+
+ def failure_message_for_should
+ if @options.key?(:expected_query_count)
+ "Expected ##{@options[:method_name]} to cause #{@options[:expected_query_count]} database queries but it actually caused #{@queries.length} queries:" + friendly_queries
+ else
+ "Expected ##{@options[:method_name]} to query the database but it actually caused #{@queries.length} queries:" + friendly_queries
+ end
+ end
+
+ def failure_message_for_should_not
+ if @options[:expected_query_count]
+ "Expected ##{@options[:method_name]} to not cause #{@options[:expected_query_count]} database queries but it actually caused #{@queries.length} queries:" + friendly_queries
+ else
+ "Expected ##{@options[:method_name]} to not query the database but it actually caused #{@queries.length} queries:" + friendly_queries
+ end
+ end
+
+ private
+
+ def friendly_queries
+ @queries.map do |query|
+ "\n (#{query[:name]}) #{query[:sql]}"
+ end.join
+ end
+
+ def filter_query(query)
+ query[:name] == 'SCHEMA' || looks_like_schema?(query[:sql])
+ end
+
+ def schema_terms
+ ['FROM sqlite_master', 'PRAGMA', 'SHOW TABLES', 'SHOW KEYS FROM', 'SHOW FIELDS FROM', 'begin transaction', 'commit transaction']
+ end
+
+ def looks_like_schema?(sql)
+ schema_terms.any? { |term| sql.include?(term) }
+ end
+ end
+ end
+ end
+end