test-prof 0.12.1 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89c1d2f72a772c731bcb32ee68a21129e82a003a40a194f6a503a31e5b445954
4
- data.tar.gz: 6ed48924861ec8523a083ba853bf5de43579df0624f95e003ca6d417c9b4e076
3
+ metadata.gz: 372bc2a293716277d1437df54387b0ce7a091c01a4fc3622f206afac9d299cc9
4
+ data.tar.gz: 9a21beb5616e03d3b7b8dd2a0d3a2ccd4bd7f5f6004f946ca0d7933ce654289c
5
5
  SHA512:
6
- metadata.gz: 7c62484546cda02e28ce819790cb3479004a970dc1c88849eb7ff3e55114b71710774f0dbc2ce7d4874036a5513f10585c3c4d805db05a75c93acb69a36436cb
7
- data.tar.gz: 4b6d8438fff041a84975c52428f50a03d9f4519a08a2884a35ae45bb43df4c11fa7f375a041477928a1009610dd0dc96d1e702ba6728eb4a2db8a6e147c3119b
6
+ metadata.gz: 7eccde7e36088e71e7b21fd3637609c4814497e438281cf87cdab75f836ee5a36beea363c7595b1060aeebb91997d2d3764999e62de3d28a8ed75998766de0c8
7
+ data.tar.gz: 3d4b46a7adde33b1939eb877867bae39f31a8abd5a7db9404ff8ad2631c79ca116dc0cec824da79223e7cf8df3075374b6fe3b939a5f1fc276f3f7319c573407
data/CHANGELOG.md CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  ## master (unrealeased)
4
4
 
5
+ ## 1.0.1 (2021-02-12)
6
+
7
+ - Fixed AnyFixture deprecation warning.
8
+
9
+ ## 1.0.0 (2021-01-21)
10
+
11
+ ## 1.0.0.rc2 (2021-01-06)
12
+
13
+ - Make Rails fixtures accesible in `before_all`. ([@palkan][])
14
+
15
+ You can load and access fixtures when explicitly enabling them via `before_all(setup_fixtures: true, &block)`.
16
+
17
+ - Minitest's `before_all` is not longer experimental. ([@palkan][])
18
+
19
+ - Add `after_all` to Minitest in addition to `before_all`. ([@palkan][])
20
+
21
+ ## 1.0.0.rc1 (2020-12-30)
22
+
23
+ - Remove deprecated `AggregateFailures` cop. ([@palkan][])
24
+
25
+ - Remove `ActiveRecordSharedConnection`. ([@palkan][])
26
+
27
+ - Add `AnyFixture#register_dump` to _cache_ fixtures using SQL dumps. ([@palkan][])
28
+
29
+ - Replaced `TestProf::AnyFixture.reporting_enabled = true` with `TestProf::AnyFixture.config.reporting_enabled = true`. ([@palkan][])
30
+
31
+ - Add support for RSpec aliases detection when linting specs using `let_it_be`/`before_all` with `rubocop-rspec` 2.0 ([@pirj][])
32
+
33
+ ## 0.12.2 (2020-09-03)
34
+
35
+ - Execute Minitest `before_all` in the context of the current test object. ([@palkan][])
36
+
5
37
  ## 0.12.1 (2020-09-01)
6
38
 
7
39
  - Minor improvements.
data/README.md CHANGED
@@ -75,7 +75,7 @@ Add `test-prof` gem to your application:
75
75
 
76
76
  ```ruby
77
77
  group :test do
78
- gem "test-prof"
78
+ gem "test-prof", "~> 1.0"
79
79
  end
80
80
  ```
81
81
 
data/config/default.yml CHANGED
@@ -18,18 +18,3 @@ RSpec/AggregateExamples:
18
18
  - validate_length_of
19
19
  - validate_inclusion_of
20
20
  - validates_exclusion_of
21
-
22
- # TODO: remove this one we hit 1.0
23
- RSpec/AggregateFailures:
24
- Description: Checks if example group contains two or more aggregatable examples.
25
- Enabled: false
26
- StyleGuide: https://rspec.rubystyle.guide/#expectation-per-example
27
- AddAggregateFailuresMetadata: true
28
- MatchersWithSideEffects:
29
- - allow_value
30
- - allow_values
31
- - validate_presence_of
32
- - validate_absence_of
33
- - validate_length_of
34
- - validate_inclusion_of
35
- - validates_exclusion_of
@@ -0,0 +1,6 @@
1
+ RSpec:
2
+ Language:
3
+ Helpers:
4
+ - let_it_be
5
+ Hooks:
6
+ - before_all
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "test_prof/ext/float_duration"
4
+ require "test_prof/any_fixture/dump"
4
5
 
5
6
  module TestProf
6
7
  # Make DB fixtures from blocks.
@@ -9,6 +10,58 @@ module TestProf
9
10
 
10
11
  using FloatDuration
11
12
 
13
+ # AnyFixture configuration
14
+ class Configuration
15
+ attr_accessor :reporting_enabled, :dumps_dir, :dump_sequence_start,
16
+ :import_dump_via_cli, :dump_matching_queries, :force_matching_dumps
17
+ attr_reader :default_dump_watch_paths
18
+
19
+ alias reporting_enabled? reporting_enabled
20
+ alias import_dump_via_cli? import_dump_via_cli
21
+
22
+ def initialize
23
+ @reporting_enabled = ENV["ANYFIXTURE_REPORT"] == "1"
24
+ @dumps_dir = "any_dumps"
25
+ @default_dump_watch_paths = %w[
26
+ db/schema.rb
27
+ db/structure.sql
28
+ ]
29
+ @dump_sequence_start = 123_654
30
+ @dump_matching_queries = /^$/
31
+ @import_dump_via_cli = ENV["ANYFIXTURE_IMPORT_DUMP_CLI"] == "1"
32
+ @before_dump = []
33
+ @after_dump = []
34
+ @force_matching_dumps =
35
+ if ENV["ANYFIXTURE_FORCE_DUMP"] == "1"
36
+ /.*/
37
+ elsif ENV["ANYFIXTURE_FORCE_DUMP"]
38
+ /#{ENV["ANYFIXTURE_FORCE_DUMP"]}/
39
+ else
40
+ /^$/
41
+ end
42
+ end
43
+
44
+ def before_dump(&block)
45
+ if block_given?
46
+ @before_dump << block
47
+ else
48
+ @before_dump
49
+ end
50
+ end
51
+
52
+ def after_dump(&block)
53
+ if block_given?
54
+ @after_dump << block
55
+ else
56
+ @after_dump
57
+ end
58
+ end
59
+
60
+ def dump_sequence_random_start
61
+ rand(dump_sequence_start..(dump_sequence_start * 2))
62
+ end
63
+ end
64
+
12
65
  class Cache # :nodoc:
13
66
  attr_reader :store, :stats
14
67
 
@@ -40,22 +93,74 @@ module TestProf
40
93
  class << self
41
94
  include Logging
42
95
 
43
- attr_accessor :reporting_enabled
96
+ def config
97
+ @config ||= Configuration.new
98
+ end
99
+
100
+ def configure
101
+ yield config
102
+ end
103
+
104
+ # Backward compatibility
105
+ def reporting_enabled=(val)
106
+ warn "AnyFixture.reporting_enabled is deprecated and will be removed in 1.1. Use AnyFixture.config.reporting_enabled instead"
107
+ config.reporting_enabled = val
108
+ end
44
109
 
45
- def reporting_enabled?
46
- reporting_enabled == true
110
+ def reporting_enabled
111
+ warn "AnyFixture.reporting_enabled is deprecated and will be removed in 1.1. Use AnyFixture.config.reporting_enabled instead"
112
+ config.reporting_enabled
47
113
  end
48
114
 
115
+ alias reporting_enabled? reporting_enabled
116
+
49
117
  # Register a block of code as a fixture,
50
118
  # returns the result of the block execution
51
119
  def register(id)
52
- cache.fetch(id) do
120
+ cached(id) do
53
121
  ActiveSupport::Notifications.subscribed(method(:subscriber), "sql.active_record") do
54
122
  yield
55
123
  end
56
124
  end
57
125
  end
58
126
 
127
+ def cached(id)
128
+ cache.fetch(id) { yield }
129
+ end
130
+
131
+ # Create and register new SQL dump.
132
+ # Use `watch` to provide additional paths to watch for
133
+ # dump re-generation
134
+ def register_dump(name, clean: true, **options)
135
+ called_from = caller_locations(1, 1).first.path
136
+ watch = options.delete(:watch) || [called_from]
137
+ cache_key = options.delete(:cache_key)
138
+ skip = options.delete(:skip_if)
139
+
140
+ id = "sql/#{name}"
141
+
142
+ register_method = clean ? :register : :cached
143
+
144
+ public_send(register_method, id) do
145
+ dump = Dump.new(name, watch: watch, cache_key: cache_key)
146
+
147
+ unless dump.force?
148
+ next if skip&.call(dump: dump)
149
+
150
+ next dump.within_prepared_env(import: true, **options) { dump.load } if dump.exists?
151
+ end
152
+
153
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record", dump.subscriber)
154
+ res = dump.within_prepared_env(**options) { yield }
155
+
156
+ dump.commit!
157
+
158
+ res
159
+ ensure
160
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
161
+ end
162
+ end
163
+
59
164
  # Clean all affected tables (but do not reset cache)
60
165
  def clean
61
166
  disable_referential_integrity do
@@ -76,7 +181,13 @@ module TestProf
76
181
 
77
182
  def subscriber(_event, _start, _finish, _id, data)
78
183
  matches = data.fetch(:sql).match(INSERT_RXP)
79
- tables_cache[matches[1]] = true if matches
184
+ return unless matches
185
+
186
+ table_name = matches[1]
187
+
188
+ return if /sqlite_sequence/.match?(table_name)
189
+
190
+ tables_cache[table_name] = true
80
191
  end
81
192
 
82
193
  def report_stats
@@ -148,7 +259,5 @@ module TestProf
148
259
  connection.disable_referential_integrity { yield }
149
260
  end
150
261
  end
151
-
152
- self.reporting_enabled = ENV["ANYFIXTURE_REPORT"] == "1"
153
262
  end
154
263
  end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/any_fixture/dump/digest"
4
+
5
+ require "set"
6
+
7
+ module TestProf
8
+ module AnyFixture
9
+ MODIFY_RXP = /^(INSERT INTO|UPDATE|DELETE FROM) ([\S]+)/i.freeze
10
+ ANY_FIXTURE_RXP = /(\/\*|\-\-).*\bany_fixture:dump/.freeze
11
+ ANY_FIXTURE_IGNORE_RXP = /(\/\*|\-\-).*\bany_fixture:ignore/.freeze
12
+
13
+ using(Module.new do
14
+ refine Object do
15
+ def to_digest
16
+ to_s
17
+ end
18
+ end
19
+
20
+ refine NilClass do
21
+ def to_digest
22
+ nil
23
+ end
24
+ end
25
+
26
+ refine Hash do
27
+ def to_digest
28
+ map { |k, v| [k.to_digest, v.to_digest].compact.join("_") }
29
+ end
30
+ end
31
+
32
+ refine Array do
33
+ def to_digest
34
+ map { |v| v.to_digest }.compact.join("-")
35
+ end
36
+ end
37
+ end)
38
+
39
+ class Dump
40
+ class Subscriber
41
+ attr_reader :path, :tmp_path
42
+
43
+ def initialize(path, adapter)
44
+ @path = path
45
+ @adapter = adapter
46
+ @tmp_path = path + ".tmp"
47
+ @reset_pk = Set.new
48
+ end
49
+
50
+ def start(_event, _id, payload)
51
+ sql = payload.fetch(:sql)
52
+ return if sql.match?(ANY_FIXTURE_IGNORE_RXP)
53
+
54
+ matches = sql.match(MODIFY_RXP)
55
+ return unless matches
56
+
57
+ reset_pk!(matches[2]) if /insert/i.match?(matches[1])
58
+ end
59
+
60
+ def finish(_event, _id, payload)
61
+ sql = payload.fetch(:sql)
62
+ return unless trackable_sql?(sql)
63
+
64
+ sql = payload[:binds].any? ? adapter.compile_sql(sql, quoted(payload[:binds])) : +sql
65
+
66
+ sql.tr!("\n", " ")
67
+
68
+ file.write(sql + ";\n")
69
+ end
70
+
71
+ def commit
72
+ return unless defined?(:@file)
73
+
74
+ file.close
75
+
76
+ FileUtils.mv(tmp_path, path)
77
+ end
78
+
79
+ private
80
+
81
+ attr_reader :reset_pk, :adapter
82
+
83
+ def file
84
+ @file ||= File.open(tmp_path, "w")
85
+ end
86
+
87
+ def reset_pk!(table_name)
88
+ return if /sqlite_sequence/.match?(table_name)
89
+
90
+ return if reset_pk.include?(table_name)
91
+
92
+ adapter.reset_sequence!(table_name, AnyFixture.config.dump_sequence_random_start)
93
+ reset_pk << table_name
94
+ end
95
+
96
+ def trackable_sql?(sql)
97
+ return false if sql.match?(ANY_FIXTURE_IGNORE_RXP)
98
+
99
+ sql.match?(MODIFY_RXP) || sql.match?(ANY_FIXTURE_RXP) || sql.match?(AnyFixture.config.dump_matching_queries)
100
+ end
101
+
102
+ def quoted(val)
103
+ if val.is_a?(Array)
104
+ val.map { |v| quoted(v) }
105
+ elsif val.is_a?(ActiveRecord::Relation::QueryAttribute)
106
+ quoted(val.value_for_database)
107
+ else
108
+ ActiveRecord::Base.connection.quote(val)
109
+ end
110
+ end
111
+ end
112
+
113
+ attr_reader :name, :digest, :path, :subscriber, :success
114
+ alias success? success
115
+
116
+ def initialize(name, watch: [], cache_key: nil)
117
+ @name = name
118
+ @digest = [
119
+ Digest.call(*watch),
120
+ cache_key.to_digest
121
+ ].compact.join("-")
122
+
123
+ @path = build_path(name, digest)
124
+
125
+ @success = false
126
+
127
+ @adapter =
128
+ case ActiveRecord::Base.connection.adapter_name
129
+ when /sqlite/i
130
+ require "test_prof/any_fixture/dump/sqlite"
131
+ SQLite.new
132
+ when /postgresql/i
133
+ require "test_prof/any_fixture/dump/postgresql"
134
+ PostgreSQL.new
135
+ else
136
+ raise ArgumentError,
137
+ "Your current database adapter (#{ActiveRecord::Base.connection.adapter_name}) " \
138
+ "is currently not supported. So far, we only support SQLite and PostgreSQL"
139
+ end
140
+
141
+ @subscriber = Subscriber.new(path, adapter)
142
+ end
143
+
144
+ def exists?
145
+ File.exist?(path)
146
+ end
147
+
148
+ def force?
149
+ AnyFixture.config.force_matching_dumps.match?(name)
150
+ end
151
+
152
+ def load
153
+ return import_via_active_record unless AnyFixture.config.import_dump_via_cli?
154
+
155
+ adapter.import(path) || import_via_active_record
156
+ end
157
+
158
+ def commit!
159
+ subscriber.commit
160
+ end
161
+
162
+ def within_prepared_env(before: nil, after: nil, import: false)
163
+ run_before_callbacks(callback: before, dump: self, import: false)
164
+ yield.tap do
165
+ @success = true
166
+ end
167
+ ensure
168
+ run_after_callbacks(callback: after, dump: self, import: false)
169
+ end
170
+
171
+ private
172
+
173
+ attr_reader :adapter
174
+
175
+ def import_via_active_record
176
+ conn = ActiveRecord::Base.connection
177
+
178
+ File.open(path).each_line do |query|
179
+ next if query.empty?
180
+
181
+ conn.execute query
182
+ end
183
+ end
184
+
185
+ def build_path(name, digest)
186
+ dir = TestProf.artifact_path(
187
+ File.join(AnyFixture.config.dumps_dir)
188
+ )
189
+
190
+ FileUtils.mkdir_p(dir)
191
+
192
+ File.join(dir, "#{name}-#{digest}.sql")
193
+ end
194
+
195
+ def run_before_callbacks(callback:, **options)
196
+ # First, call config-defined setup callbacks
197
+ AnyFixture.config.before_dump.each { |clbk| clbk.call(**options) }
198
+ # Then, adapter-defined callbacks
199
+ adapter.setup_env unless options[:import]
200
+ # Finally, user-provided callback
201
+ callback&.call(**options)
202
+ end
203
+
204
+ def run_after_callbacks(callback:, **options)
205
+ # The order is vice versa to setup
206
+ callback&.call(**options)
207
+ adapter.teardown_env unless options[:import]
208
+ AnyFixture.config.after_dump.each { |clbk| clbk.call(**options) }
209
+ end
210
+ end
211
+ end
212
+ end
@@ -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
@@ -32,6 +32,12 @@ module TestProf
32
32
  end
33
33
  end
34
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
+
35
41
  def config
36
42
  @config ||= Configuration.new
37
43
  end
@@ -60,8 +66,11 @@ module TestProf
60
66
  class Configuration
61
67
  HOOKS = %i[begin rollback].freeze
62
68
 
69
+ attr_accessor :setup_fixtures
70
+
63
71
  def initialize
64
72
  @hooks = Hash.new { |h, k| h[k] = HooksChain.new(k) }
73
+ @setup_fixtures = false
65
74
  end
66
75
 
67
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
@@ -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
@@ -15,7 +15,7 @@ RSpec.configure do |config|
15
15
  config.include_context "any_fixture:clean", with_clean_fixture: true
16
16
 
17
17
  config.after(:suite) do
18
- TestProf::AnyFixture.report_stats if TestProf::AnyFixture.reporting_enabled?
18
+ TestProf::AnyFixture.report_stats if TestProf::AnyFixture.config.reporting_enabled?
19
19
  TestProf::AnyFixture.reset
20
20
  end
21
21
  end
@@ -6,14 +6,22 @@ module TestProf
6
6
  module BeforeAll
7
7
  # Helper to wrap the whole example group into a transaction
8
8
  module RSpec
9
- def before_all(&block)
9
+ def before_all(setup_fixtures: BeforeAll.config.setup_fixtures, &block)
10
10
  raise ArgumentError, "Block is required!" unless block_given?
11
11
 
12
- return before(:all, &block) if within_before_all?
12
+ if within_before_all?
13
+ before(:all) do
14
+ @__inspect_output = "before_all hook"
15
+ instance_eval(&block)
16
+ end
17
+ return
18
+ end
13
19
 
14
20
  @__before_all_activated__ = true
15
21
 
16
22
  before(:all) do
23
+ @__inspect_output = "before_all hook"
24
+ BeforeAll.setup_fixtures(self) if setup_fixtures
17
25
  BeforeAll.begin_transaction do
18
26
  instance_eval(&block)
19
27
  end
@@ -108,8 +108,9 @@ module TestProf
108
108
 
109
109
  LetItBe.module_for(self).module_eval do
110
110
  define_method(identifier) do
111
- # Trying to detect the context (couldn't find other way so far)
112
- if /\(:context\)/.match?(@__inspect_output)
111
+ # Trying to detect the context
112
+ # Based on https://github.com/rspec/rspec-rails/commit/7cb796db064f58da7790a92e73ab906ef50b1f34
113
+ if @__inspect_output.include?("before(:context)") || @__inspect_output.include?("before_all")
113
114
  instance_variable_get(:"#{PREFIX}#{identifier}")
114
115
  else
115
116
  # Fallback to let definition
@@ -11,4 +11,3 @@ require "rubocop"
11
11
 
12
12
  require_relative "cops/inject"
13
13
  require "test_prof/cops/rspec/aggregate_examples"
14
- require "test_prof/cops/rspec/aggregate_failures"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TestProf
4
- VERSION = "0.12.1"
4
+ VERSION = "1.0.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-prof
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-01 00:00:00.000000000 Z
11
+ date: 2021-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -119,6 +119,7 @@ files:
119
119
  - assets/tagprof.demo.html
120
120
  - assets/tagprof.template.html
121
121
  - config/default.yml
122
+ - config/rubocop-rspec.yml
122
123
  - lib/minitest/base_reporter.rb
123
124
  - lib/minitest/event_prof_formatter.rb
124
125
  - lib/minitest/test_prof_plugin.rb
@@ -126,6 +127,11 @@ files:
126
127
  - lib/test_prof.rb
127
128
  - lib/test_prof/any_fixture.rb
128
129
  - lib/test_prof/any_fixture/dsl.rb
130
+ - lib/test_prof/any_fixture/dump.rb
131
+ - lib/test_prof/any_fixture/dump/base_adapter.rb
132
+ - lib/test_prof/any_fixture/dump/digest.rb
133
+ - lib/test_prof/any_fixture/dump/postgresql.rb
134
+ - lib/test_prof/any_fixture/dump/sqlite.rb
129
135
  - lib/test_prof/before_all.rb
130
136
  - lib/test_prof/before_all/adapters/active_record.rb
131
137
  - lib/test_prof/before_all/isolator.rb
@@ -136,7 +142,6 @@ files:
136
142
  - lib/test_prof/cops/rspec/aggregate_examples/matchers_with_side_effects.rb
137
143
  - lib/test_prof/cops/rspec/aggregate_examples/metadata_helpers.rb
138
144
  - lib/test_prof/cops/rspec/aggregate_examples/node_matchers.rb
139
- - lib/test_prof/cops/rspec/aggregate_failures.rb
140
145
  - lib/test_prof/cops/rspec/language.rb
141
146
  - lib/test_prof/event_prof.rb
142
147
  - lib/test_prof/event_prof/custom_events.rb
@@ -148,7 +153,6 @@ files:
148
153
  - lib/test_prof/event_prof/monitor.rb
149
154
  - lib/test_prof/event_prof/profiler.rb
150
155
  - lib/test_prof/event_prof/rspec.rb
151
- - lib/test_prof/ext/active_record_3.rb
152
156
  - lib/test_prof/ext/active_record_refind.rb
153
157
  - lib/test_prof/ext/array_bsearch_index.rb
154
158
  - lib/test_prof/ext/factory_bot_strategy.rb
@@ -173,8 +177,6 @@ files:
173
177
  - lib/test_prof/factory_prof/printers/flamegraph.rb
174
178
  - lib/test_prof/factory_prof/printers/simple.rb
175
179
  - lib/test_prof/logging.rb
176
- - lib/test_prof/recipes/active_record_one_love.rb
177
- - lib/test_prof/recipes/active_record_shared_connection.rb
178
180
  - lib/test_prof/recipes/logging.rb
179
181
  - lib/test_prof/recipes/minitest/before_all.rb
180
182
  - lib/test_prof/recipes/minitest/sample.rb
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RuboCop
4
- module Cop
5
- module RSpec
6
- class AggregateExamples
7
- def self.registry
8
- RuboCop::Cop::Cop.registry
9
- end
10
- end
11
-
12
- class AggregateFailures < AggregateExamples
13
- def initialize(*)
14
- super
15
- self.class.just_once { warn "`AggregateFailures` cop has been renamed to `AggregateExamples`." }
16
- end
17
-
18
- def self.just_once
19
- return if @already_done
20
- yield
21
- @already_done = true
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TestProf
4
- # Add missing `begin_transaction` and `rollback_transaction` methods
5
- module ActiveRecord3Transactions
6
- refine ::ActiveRecord::ConnectionAdapters::AbstractAdapter do
7
- def begin_transaction(joinable: true)
8
- if open_transactions > 0
9
- increment_open_transactions
10
- create_savepoint
11
- else
12
- begin_db_transaction
13
- end
14
- self.transaction_joinable = joinable
15
- end
16
-
17
- def rollback_transaction(*)
18
- if open_transactions > 1
19
- rollback_to_savepoint
20
- else
21
- rollback_db_transaction
22
- end
23
- decrement_open_transactions
24
- end
25
- end
26
- end
27
- end
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "./active_record_shared_connection"
4
-
5
- # One ❤️
6
- TestProf::ActiveRecordOneLove = TestProf::ActiveRecordSharedConnection
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TestProf
4
- # Forces ActiveRecord to use the same connection between threads
5
- module ActiveRecordSharedConnection # :nodoc: all
6
- class << self
7
- attr_reader :connection
8
-
9
- def enable!
10
- self.connection = ActiveRecord::Base.connection
11
- end
12
-
13
- def disable!
14
- self.connection = nil
15
- end
16
-
17
- def ignore(&block)
18
- raise ArgumentError, "Block is required" unless block_given?
19
-
20
- @ignores ||= []
21
-
22
- ignores << block
23
- end
24
-
25
- def ignored?(config)
26
- !ignores.nil? && ignores.any? { |clbk| clbk.call(config) }
27
- end
28
-
29
- private
30
-
31
- attr_reader :ignores
32
-
33
- def connection=(conn)
34
- @connection = conn
35
- connection.singleton_class.prepend Connection
36
- connection
37
- end
38
- end
39
-
40
- module Connection
41
- def shared_lock
42
- @shared_lock ||= Mutex.new
43
- end
44
-
45
- def exec_cache(*)
46
- shared_lock.synchronize { super }
47
- end
48
-
49
- def exec_no_cache(*)
50
- shared_lock.synchronize { super }
51
- end
52
-
53
- def execute(*)
54
- shared_lock.synchronize { super }
55
- end
56
- end
57
-
58
- module Ext
59
- def connection
60
- return super if ActiveRecordSharedConnection.ignored?(connection_config)
61
- ActiveRecordSharedConnection.connection || super
62
- end
63
- end
64
- end
65
- end
66
-
67
- ActiveSupport.on_load(:active_record) do
68
- if ::ActiveRecord::Base.connection.pool.respond_to?(:lock_thread=)
69
- TestProf.log :warn, "You activated ActiveRecordSharedConnection patch for the Rails version,\n" \
70
- "which has a built-in support for the same functionality.\n" \
71
- "Consider removing it, 'cause this could result in unexpected behaviour.\n\n" \
72
- "Read more in the docs: https://test-prof.evilmartians.io/#/active_record_shared_connection"
73
- end
74
-
75
- TestProf::ActiveRecordSharedConnection.enable!
76
- ActiveRecord::Base.singleton_class.prepend TestProf::ActiveRecordSharedConnection::Ext
77
- end