test-prof 0.11.3 → 1.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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