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