spec: make specs resilient to pre-existing records, refs SD-1570

in jenkins-land we've historically used db snapshots so the db will be
empty. but if you `rake db:migrate` your test db, you'll have things like
default accounts, notifications, etc., which can break certain specs

Change-Id: I175e651d1b50671e630dc0181d1c4d25e78abe97
Reviewed-on: https://gerrit.instructure.com/95249
Reviewed-by: Landon Wilkins <lwilkins@instructure.com>
Product-Review: Landon Wilkins <lwilkins@instructure.com>
QA-Review: Landon Wilkins <lwilkins@instructure.com>
Tested-by: Jenkins
This commit is contained in:
Jon Jensen 2016-11-15 11:20:55 -07:00
parent 26242baa7f
commit 82eac2107c
2 changed files with 67 additions and 38 deletions

View File

@ -60,19 +60,15 @@ module WebMock::API
end end
end end
# ensure people aren't creating records outside the rspec lifecycle, e.g. # nuke the db (say, if `rake db:migrate RAILS_ENV=test` created records),
# inside a describe/context block rather than a let/before/example # and then ensure people aren't creating records outside the rspec
# lifecycle, e.g. inside a describe/context block rather than a
# let/before/example
require_relative 'support/blank_slate_protection' require_relative 'support/blank_slate_protection'
BlankSlateProtection.enable! BlankSlateProtection.install!
require_relative 'support/discourage_slow_specs' require_relative 'support/discourage_slow_specs'
RSpec::Core::ExampleGroup.singleton_class.prepend(Module.new {
def run_examples(*)
BlankSlateProtection.disable { super }
end
})
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
ActionView::TestCase::TestController.view_paths = ApplicationController.view_paths ActionView::TestCase::TestController.view_paths = ApplicationController.view_paths

View File

@ -1,28 +1,36 @@
require_relative "./call_stack_utils" require_relative "./call_stack_utils"
module BlankSlateProtection module BlankSlateProtection
def create_or_update module ActiveRecord
return super unless BlankSlateProtection.enabled? def create_or_update
return super if caller.grep(BlankSlateProtection.exempt_patterns).present? return super unless BlankSlateProtection.enabled?
return super if caller.grep(BlankSlateProtection.exempt_patterns).present?
location = CallStackUtils.best_line_for(caller).sub(/:in .*/, '') location = CallStackUtils.best_line_for(caller).sub(/:in .*/, '')
if caller.grep(/_context_hooks/).present? if caller.grep(/_context_hooks/).present?
$stderr.puts "\e[31mError: Don't create records inside `:all` hooks!" $stderr.puts "\e[31mError: Don't create records inside `:all` hooks!"
$stderr.puts "See: " + location + "\e[0m" $stderr.puts "See: " + location + "\e[0m"
$stderr.puts
$stderr.puts "\e[33mTIP:\e[0m change this to `:each`, or if you are really concerned"
$stderr.puts "about performance, use `:once`. `:all` hooks are dangerous because"
$stderr.puts "they can leave around garbage that affects later specs"
else
$stderr.puts "\e[31mError: Don't create records outside the rspec lifecycle!"
$stderr.puts "See: " + location + "\e[0m"
$stderr.puts
$stderr.puts "\e[33mTIP:\e[0m move this into a `before`, `let` or `it`. Otherwise it will exist"
$stderr.puts "before *any* specs start, and possibly be deleted/modified before the"
$stderr.puts "spec that needs it actually runs."
end
$stderr.puts $stderr.puts
$stderr.puts "\e[33mTIP:\e[0m change this to `:each`, or if you are really concerned" exit! 1
$stderr.puts "about performance, use `:once`. `:all` hooks are dangerous because" end
$stderr.puts "they can leave around garbage that affects later specs" end
else
$stderr.puts "\e[31mError: Don't create records outside the rspec lifecycle!" module ExampleGroup
$stderr.puts "See: " + location + "\e[0m" def run_examples(*)
$stderr.puts BlankSlateProtection.disable { super }
$stderr.puts "\e[33mTIP:\e[0m move this into a `before`, `let` or `it`. Otherwise it will exist"
$stderr.puts "before *any* specs start, and possibly be deleted/modified before the"
$stderr.puts "spec that needs it actually runs."
end end
$stderr.puts
exit! 1
end end
# switchman and once-ler have special snowflake context hooks where data # switchman and once-ler have special snowflake context hooks where data
@ -34,26 +42,51 @@ module BlankSlateProtection
@enabled @enabled
end end
def enable! def install!
truncate_all_tables!
::RSpec::Core::ExampleGroup.singleton_class.prepend ExampleGroup
::ActiveRecord::Base.include ActiveRecord
@enabled = true @enabled = true
end end
def disable!
@enabled = false
end
def disable def disable
enabled = @enabled @enabled = false
disable!
yield yield
ensure ensure
@enabled = enabled @enabled = true
end end
def exempt_patterns def exempt_patterns
Regexp.new(EXEMPT_PATTERNS.map { |pattern| Regexp.escape(pattern) }.join("|")) Regexp.new(EXEMPT_PATTERNS.map { |pattern| Regexp.escape(pattern) }.join("|"))
end end
def get_table_names(connection)
# use custom SQL to exclude tables from extensions
schema = connection.shard.name if connection.instance_variable_get(:@config)[:use_qualified_names]
table_names = connection.query(<<-SQL, 'SCHEMA').map(&:first)
SELECT relname
FROM pg_class INNER JOIN pg_namespace ON relnamespace=pg_namespace.oid
WHERE nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
AND relkind='r'
AND NOT EXISTS (
SELECT 1 FROM pg_depend WHERE deptype='e' AND objid=pg_class.oid
)
SQL
table_names.delete('schema_migrations')
table_names
end
def truncate_all_tables!(quick: true)
return if quick && Account.all.empty? # this is the most likely table to have stuff
puts "truncating all tables..."
Shard.with_each_shard do
model_connections = ::ActiveRecord::Base.descendants.map(&:connection).uniq
model_connections.each do |connection|
table_names = get_table_names(connection)
next if table_names.empty?
connection.execute("TRUNCATE TABLE #{table_names.map { |t| connection.quote_table_name(t) }.join(',')}")
end
end
end
end end
end end
ActiveRecord::Base.include BlankSlateProtection