test-prof 0.11.3 → 1.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +122 -447
  3. data/LICENSE.txt +1 -1
  4. data/README.md +9 -13
  5. data/config/default.yml +0 -15
  6. data/config/rubocop-rspec.yml +6 -0
  7. data/lib/minitest/test_prof_plugin.rb +3 -0
  8. data/lib/test_prof/any_fixture.rb +116 -7
  9. data/lib/test_prof/any_fixture/dump.rb +207 -0
  10. data/lib/test_prof/any_fixture/dump/base_adapter.rb +43 -0
  11. data/lib/test_prof/any_fixture/dump/digest.rb +29 -0
  12. data/lib/test_prof/any_fixture/dump/postgresql.rb +91 -0
  13. data/lib/test_prof/any_fixture/dump/sqlite.rb +42 -0
  14. data/lib/test_prof/before_all.rb +9 -4
  15. data/lib/test_prof/before_all/adapters/active_record.rb +14 -5
  16. data/lib/test_prof/cops/rspec/aggregate_examples.rb +2 -2
  17. data/lib/test_prof/cops/rspec/aggregate_examples/its.rb +1 -1
  18. data/lib/test_prof/cops/rspec/aggregate_examples/line_range_helpers.rb +1 -1
  19. data/lib/test_prof/cops/rspec/aggregate_examples/matchers_with_side_effects.rb +1 -1
  20. data/lib/test_prof/cops/rspec/aggregate_examples/metadata_helpers.rb +1 -1
  21. data/lib/test_prof/cops/rspec/aggregate_examples/node_matchers.rb +1 -1
  22. data/lib/test_prof/event_prof/instrumentations/active_support.rb +22 -4
  23. data/lib/test_prof/recipes/minitest/before_all.rb +48 -23
  24. data/lib/test_prof/recipes/minitest/sample.rb +6 -10
  25. data/lib/test_prof/recipes/rspec/before_all.rb +10 -10
  26. data/lib/test_prof/recipes/rspec/let_it_be.rb +111 -13
  27. data/lib/test_prof/recipes/rspec/sample.rb +4 -2
  28. data/lib/test_prof/rubocop.rb +0 -1
  29. data/lib/test_prof/stack_prof.rb +3 -0
  30. data/lib/test_prof/version.rb +1 -1
  31. metadata +23 -21
  32. data/lib/test_prof/cops/rspec/aggregate_failures.rb +0 -26
  33. data/lib/test_prof/ext/active_record_3.rb +0 -27
  34. data/lib/test_prof/recipes/active_record_one_love.rb +0 -6
  35. data/lib/test_prof/recipes/active_record_shared_connection.rb +0 -77
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ module AnyFixture
5
+ class Dump
6
+ class BaseAdapter
7
+ def reset_sequence!(_table_name, _start)
8
+ end
9
+
10
+ def compile_sql(sql, _binds)
11
+ sql
12
+ end
13
+
14
+ def setup_env
15
+ end
16
+
17
+ def teardown_env
18
+ end
19
+
20
+ def import(_path)
21
+ false
22
+ end
23
+
24
+ private
25
+
26
+ def while_disconnected
27
+ conn.disconnect!
28
+ yield
29
+ ensure
30
+ conn.reconnect!
31
+ end
32
+
33
+ def conn
34
+ ActiveRecord::Base.connection
35
+ end
36
+
37
+ def execute(query)
38
+ conn.execute(query)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+
5
+ module TestProf
6
+ module AnyFixture
7
+ class Dump
8
+ module Digest
9
+ module_function
10
+
11
+ def call(*paths)
12
+ files = (AnyFixture.config.default_dump_watch_paths + paths).each_with_object([]) do |path_or_glob, acc|
13
+ if File.file?(path_or_glob)
14
+ acc << path_or_glob
15
+ else
16
+ acc = acc.concat Dir[path_or_glob]
17
+ end
18
+ acc
19
+ end
20
+
21
+ return if files.empty?
22
+
23
+ file_ids = files.sort.map { |f| "#{File.basename(f)}/#{::Digest::SHA1.file(f).hexdigest}" }
24
+ ::Digest::SHA1.hexdigest(file_ids.join("/"))
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/any_fixture/dump/base_adapter"
4
+
5
+ module TestProf
6
+ module AnyFixture
7
+ class Dump
8
+ class PostgreSQL < BaseAdapter
9
+ UUID_FUNCTIONS = %w[
10
+ gen_random_uuid
11
+ uuid_generate_v4
12
+ ]
13
+
14
+ def reset_sequence!(table_name, start)
15
+ _pk, sequence = conn.pk_and_sequence_for(table_name)
16
+ return unless sequence
17
+
18
+ sequence_name = "#{sequence.schema}.#{sequence.identifier}"
19
+
20
+ execute <<~SQL
21
+ ALTER SEQUENCE #{sequence_name} RESTART WITH #{start}; -- any_fixture:dump
22
+ SQL
23
+ end
24
+
25
+ def compile_sql(sql, binds)
26
+ sql.gsub(/\$\d+/) { binds.shift }
27
+ end
28
+
29
+ def import(path)
30
+ # Test if psql is installed
31
+ `psql --version`
32
+
33
+ tasks = ActiveRecord::Tasks::PostgreSQLDatabaseTasks.new(conn.pool.spec.config.with_indifferent_access)
34
+
35
+ while_disconnected do
36
+ tasks.structure_load(path, "--output=/dev/null")
37
+ end
38
+
39
+ true
40
+ rescue Errno::ENOENT
41
+ false
42
+ end
43
+
44
+ def setup_env
45
+ # Mock UUID generating functions to provide consistent results
46
+ quoted_functions = UUID_FUNCTIONS.map { |func| "'#{func}'" }.join(", ")
47
+
48
+ @uuid_funcs = execute <<~SQL
49
+ SELECT
50
+ pp.proname, pn.nspname,
51
+ pg_get_functiondef(pp.oid) AS definition
52
+ FROM pg_proc pp
53
+ JOIN pg_namespace pn
54
+ ON pn.oid = pp.pronamespace
55
+ WHERE pp.proname in (#{quoted_functions})
56
+ ORDER BY pp.oid;
57
+ SQL
58
+
59
+ uuid_funcs.each do |(func, ns, _)|
60
+ execute <<~SQL
61
+ CREATE OR REPLACE FUNCTION #{ns}.#{func}()
62
+ RETURNS UUID
63
+ LANGUAGE SQL
64
+ AS $$
65
+ SELECT md5(random()::TEXT)::UUID;
66
+ $$; -- any_fixture:dump
67
+ SQL
68
+ end
69
+
70
+ execute <<~SQL
71
+ SELECT setseed(#{rand}); -- any_fixture:dump
72
+ SQL
73
+ end
74
+
75
+ def teardown_env
76
+ uuid_funcs.each do |(func, ns, definition)|
77
+ execute "#{definition}; -- any_fixture:dump"
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ attr_reader :uuid_funcs
84
+
85
+ def execute(query)
86
+ super.values
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/any_fixture/dump/base_adapter"
4
+
5
+ module TestProf
6
+ module AnyFixture
7
+ class Dump
8
+ class SQLite < BaseAdapter
9
+ def reset_sequence!(table_name, start)
10
+ execute <<~SQL.chomp
11
+ DELETE FROM sqlite_sequence WHERE name=#{table_name}
12
+ SQL
13
+
14
+ execute <<~SQL.chomp
15
+ INSERT INTO sqlite_sequence (name, seq)
16
+ VALUES (#{table_name}, #{start})
17
+ SQL
18
+ end
19
+
20
+ def compile_sql(sql, binds)
21
+ sql.gsub(/\?/) { binds.shift }
22
+ end
23
+
24
+ def import(path)
25
+ db = conn.pool.spec.config[:database]
26
+ return false if %r{:memory:}.match?(db)
27
+
28
+ # Check that sqlite3 is installed
29
+ `sqlite3 --version`
30
+
31
+ while_disconnected do
32
+ `sqlite3 #{db} < "#{path}"`
33
+ end
34
+
35
+ true
36
+ rescue Errno::ENOENT
37
+ false
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -24,10 +24,6 @@ module TestProf
24
24
  yield
25
25
  end
26
26
 
27
- def within_transaction
28
- yield
29
- end
30
-
31
27
  def rollback_transaction
32
28
  raise AdapterMissing if adapter.nil?
33
29
 
@@ -36,6 +32,12 @@ module TestProf
36
32
  end
37
33
  end
38
34
 
35
+ def setup_fixtures(test_object)
36
+ raise ArgumentError, "Current adapter doesn't support #setup_fixtures" unless adapter.respond_to?(:setup_fixtures)
37
+
38
+ adapter.setup_fixtures(test_object)
39
+ end
40
+
39
41
  def config
40
42
  @config ||= Configuration.new
41
43
  end
@@ -64,8 +66,11 @@ module TestProf
64
66
  class Configuration
65
67
  HOOKS = %i[begin rollback].freeze
66
68
 
69
+ attr_accessor :setup_fixtures
70
+
67
71
  def initialize
68
72
  @hooks = Hash.new { |h, k| h[k] = HooksChain.new(k) }
73
+ @setup_fixtures = false
69
74
  end
70
75
 
71
76
  # Add `before` hook for `begin` or
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if ::ActiveRecord::VERSION::MAJOR < 4
4
- require "test_prof/ext/active_record_3"
5
- using TestProf::ActiveRecord3Transactions
6
- end
7
-
8
3
  module TestProf
9
4
  module BeforeAll
10
5
  module Adapters
@@ -23,6 +18,20 @@ module TestProf
23
18
  end
24
19
  ::ActiveRecord::Base.connection.rollback_transaction
25
20
  end
21
+
22
+ def setup_fixtures(test_object)
23
+ test_object.instance_eval do
24
+ @@already_loaded_fixtures ||= {}
25
+ @fixture_cache ||= {}
26
+
27
+ if @@already_loaded_fixtures[self.class]
28
+ @loaded_fixtures = @@already_loaded_fixtures[self.class]
29
+ else
30
+ @loaded_fixtures = load_fixtures(config)
31
+ @@already_loaded_fixtures[self.class] = @loaded_fixtures
32
+ end
33
+ end
34
+ end
26
35
  end
27
36
  end
28
37
  end
@@ -12,7 +12,7 @@ module RuboCop
12
12
  module RSpec
13
13
  # Checks if example groups contain two or more aggregatable examples.
14
14
  #
15
- # @see https://github.com/rubocop-hq/rspec-style-guide#expectations-per-example
15
+ # @see https://github.com/rubocop-hq/rspec-style-guide#expectation-per-example
16
16
  #
17
17
  # This cop is primarily for reducing the cost of repeated expensive
18
18
  # context initialization.
@@ -108,7 +108,7 @@ module RuboCop
108
108
  # expect(number).to be_odd
109
109
  # end
110
110
  #
111
- class AggregateExamples < Cop
111
+ class AggregateExamples < ::RuboCop::Cop::Cop
112
112
  include LineRangeHelpers
113
113
  include MetadataHelpers
114
114
  include NodeMatchers
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
- class AggregateExamples < Cop
6
+ class AggregateExamples < ::RuboCop::Cop::Cop
7
7
  # @example `its`
8
8
  #
9
9
  # # Supports regular `its` call with an attribute/method name,
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
- class AggregateExamples < Cop
6
+ class AggregateExamples < ::RuboCop::Cop::Cop
7
7
  # @internal Support methods for keeping newlines around examples.
8
8
  module LineRangeHelpers
9
9
  include RangeHelp
@@ -5,7 +5,7 @@ require_relative "../language"
5
5
  module RuboCop
6
6
  module Cop
7
7
  module RSpec
8
- class AggregateExamples < Cop
8
+ class AggregateExamples < ::RuboCop::Cop::Cop
9
9
  # When aggregated, the expectations will fail when not supposed to or
10
10
  # have a risk of not failing when expected to. One example is
11
11
  # `validate_presence_of :comment` as it leaves an empty comment after
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
- class AggregateExamples < Cop
6
+ class AggregateExamples < ::RuboCop::Cop::Cop
7
7
  # @internal
8
8
  # Support methods for example metadata.
9
9
  # Examples with similar metadata are grouped.
@@ -5,7 +5,7 @@ require_relative "../language"
5
5
  module RuboCop
6
6
  module Cop
7
7
  module RSpec
8
- class AggregateExamples < Cop
8
+ class AggregateExamples < ::RuboCop::Cop::Cop
9
9
  # @internal
10
10
  # Node matchers and searchers.
11
11
  module NodeMatchers
@@ -4,13 +4,31 @@ module TestProf::EventProf
4
4
  module Instrumentations
5
5
  # Wrapper over ActiveSupport::Notifications
6
6
  module ActiveSupport
7
+ class Subscriber
8
+ attr_reader :block, :started_at
9
+
10
+ def initialize(block)
11
+ @block = block
12
+ end
13
+
14
+ def start(*)
15
+ @started_at = TestProf.now
16
+ end
17
+
18
+ def publish(_name, started_at, finished_at, *)
19
+ block.call(finished_at - started_at)
20
+ end
21
+
22
+ def finish(*)
23
+ block.call(TestProf.now - started_at)
24
+ end
25
+ end
26
+
7
27
  class << self
8
- def subscribe(event)
28
+ def subscribe(event, &block)
9
29
  raise ArgumentError, "Block is required!" unless block_given?
10
30
 
11
- ::ActiveSupport::Notifications.subscribe(event) do |_event, start, finish, *_args|
12
- yield (finish - start)
13
- end
31
+ ::ActiveSupport::Notifications.subscribe(event, Subscriber.new(block))
14
32
  end
15
33
 
16
34
  def instrument(event)
@@ -8,41 +8,59 @@ module TestProf
8
8
  # store instance variables
9
9
  module Minitest # :nodoc: all
10
10
  class Executor
11
- attr_reader :active
11
+ attr_reader :active, :block, :captured_ivars, :teardown_block, :current_test_object,
12
+ :setup_fixtures
12
13
 
13
14
  alias active? active
15
+ alias setup_fixtures? setup_fixtures
14
16
 
15
- def initialize(&block)
17
+ def initialize(setup_fixtures: false, &block)
18
+ @setup_fixtures = setup_fixtures
16
19
  @block = block
20
+ @captured_ivars = []
17
21
  end
18
22
 
19
- def activate!(test_class)
20
- return if active?
23
+ def teardown(&block)
24
+ @teardown_block = block
25
+ end
26
+
27
+ def activate!(test_object)
28
+ @current_test_object = test_object
29
+
30
+ return restore_ivars(test_object) if active?
21
31
  @active = true
22
- @examples_left = test_class.runnable_methods.size
32
+ BeforeAll.setup_fixtures(test_object) if setup_fixtures?
23
33
  BeforeAll.begin_transaction do
24
- capture!
34
+ capture!(test_object)
25
35
  end
26
36
  end
27
37
 
28
- def try_deactivate!
29
- @examples_left -= 1
30
- return unless @examples_left.zero?
31
-
38
+ def deactivate!
32
39
  @active = false
40
+
41
+ current_test_object&.instance_eval(&teardown_block) if teardown_block
42
+
43
+ @current_test_object = nil
33
44
  BeforeAll.rollback_transaction
34
45
  end
35
46
 
36
- def capture!
37
- instance_eval(&@block)
47
+ def capture!(test_object)
48
+ return unless block
49
+
50
+ before_ivars = test_object.instance_variables
51
+
52
+ test_object.instance_eval(&block)
53
+
54
+ (test_object.instance_variables - before_ivars).each do |ivar|
55
+ captured_ivars << [ivar, test_object.instance_variable_get(ivar)]
56
+ end
38
57
  end
39
58
 
40
- def restore_to(test_object)
41
- instance_variables.each do |ivar|
42
- next if ivar == :@block
59
+ def restore_ivars(test_object)
60
+ captured_ivars.each do |(ivar, val)|
43
61
  test_object.instance_variable_set(
44
62
  ivar,
45
- instance_variable_get(ivar)
63
+ val
46
64
  )
47
65
  end
48
66
  end
@@ -57,22 +75,29 @@ module TestProf
57
75
  module ClassMethods
58
76
  attr_accessor :before_all_executor
59
77
 
60
- def before_all(&block)
61
- self.before_all_executor = Executor.new(&block)
78
+ def before_all(setup_fixtures: BeforeAll.config.setup_fixtures, &block)
79
+ self.before_all_executor = Executor.new(setup_fixtures: setup_fixtures, &block)
62
80
 
63
81
  prepend(Module.new do
64
- def setup
65
- self.class.before_all_executor.activate!(self.class)
66
- self.class.before_all_executor.restore_to(self)
82
+ def before_setup
83
+ self.class.before_all_executor.activate!(self)
67
84
  super
68
85
  end
86
+ end)
69
87
 
70
- def teardown
88
+ singleton_class.prepend(Module.new do
89
+ def run(*)
71
90
  super
72
- self.class.before_all_executor.try_deactivate!
91
+ ensure
92
+ before_all_executor&.deactivate!
73
93
  end
74
94
  end)
75
95
  end
96
+
97
+ def after_all(&block)
98
+ self.before_all_executor ||= Executor.new
99
+ before_all_executor.teardown(&block)
100
+ end
76
101
  end
77
102
  end
78
103
  end