test-prof 0.12.0 → 1.0.0

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.
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](https://cultofmartians.com)
2
- [![Gem Version](https://badge.fury.io/rb/test-prof.svg)](https://rubygems.org/gems/test-prof) [![Build](https://github.com/palkan/test-prof/workflows/Build/badge.svg)](https://github.com/palkan/test-prof/actions)
3
- [![JRuby Build](https://github.com/palkan/test-prof/workflows/JRuby%20Build/badge.svg)](https://github.com/palkan/test-prof/actions)
4
- [![Code Triagers Badge](https://www.codetriage.com/palkan/test-prof/badges/users.svg)](https://www.codetriage.com/palkan/test-prof)
2
+ [![Gem Version](https://badge.fury.io/rb/test-prof.svg)](https://rubygems.org/gems/test-prof) [![Build](https://github.com/test-prof/test-prof/workflows/Build/badge.svg)](https://github.com/test-prof/test-prof/actions)
3
+ [![JRuby Build](https://github.com/test-prof/test-prof/workflows/JRuby%20Build/badge.svg)](https://github.com/test-prof/test-prof/actions)
4
+ [![Code Triagers Badge](https://www.codetriage.com/test-prof/test-prof/badges/users.svg)](https://www.codetriage.com/test-prof/test-prof)
5
5
  [![Documentation](https://img.shields.io/badge/docs-link-brightgreen.svg)](https://test-prof.evilmartians.io)
6
6
 
7
7
  # Ruby Tests Profiling Toolbox
@@ -51,7 +51,7 @@ TestProf toolbox aims to help you identify bottlenecks in your test suite. It co
51
51
  - [CodeTriage](https://github.com/codetriage/codetriage)
52
52
  - [Dev.to](https://github.com/thepracticaldev/dev.to)
53
53
  - [Open Project](https://github.com/opf/openproject)
54
- - [...and others](https://github.com/palkan/test-prof/issues/73)
54
+ - [...and others](https://github.com/test-prof/test-prof/issues/73)
55
55
 
56
56
  ## Resources
57
57
 
@@ -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
 
@@ -83,7 +83,7 @@ And that's it)
83
83
 
84
84
  Supported Ruby versions:
85
85
 
86
- - Ruby (MRI) >= 2.4.0 (**NOTE:** for Ruby 2.2 use TestProf < 0.7.0 or Ruby 2.3 use TestProf ~> 0.7.0)
86
+ - Ruby (MRI) >= 2.5.0 (**NOTE:** for Ruby 2.2 use TestProf < 0.7.0, Ruby 2.3 use TestProf ~> 0.7.0, Ruby 2.4 use TestProf <0.12.0)
87
87
 
88
88
  - JRuby >= 9.1.0.0 (**NOTE:** refinements-dependent features might require 9.2.7+)
89
89
 
@@ -95,9 +95,9 @@ Check out our [docs][].
95
95
 
96
96
  ## What's next?
97
97
 
98
- Have an idea? [Propose](https://github.com/palkan/test-prof/issues/new) a feature request!
98
+ Have an idea? [Propose](https://github.com/test-prof/test-prof/issues/new) a feature request!
99
99
 
100
- Already using TestProf? [Share your story!](https://github.com/palkan/test-prof/issues/73)
100
+ Already using TestProf? [Share your story!](https://github.com/test-prof/test-prof/issues/73)
101
101
 
102
102
  ## License
103
103
 
@@ -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