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
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2017-2019 palkan
3
+ Copyright (c) 2017-2020 Vladimir Dementyev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
- [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](http://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)
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/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
@@ -47,11 +47,11 @@ TestProf toolbox aims to help you identify bottlenecks in your test suite. It co
47
47
  ## Who uses TestProf
48
48
 
49
49
  - [Discourse](https://github.com/discourse/discourse) reduced [~27% of their test suite time](https://twitter.com/samsaffron/status/1125602558024699904)
50
- - [Gitlab](https://gitlab.com/gitlab-org/gitlab-ce) reduced [39% of their API tests time](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14370)
50
+ - [Gitlab](https://gitlab.com/gitlab-org/gitlab-ce) reduced [39% of their API tests time](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14370) and [improved factories usage](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26810)
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
 
@@ -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,16 +95,12 @@ 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
 
104
104
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
105
105
 
106
106
  [docs]: https://test-prof.evilmartians.io
107
-
108
- ## Security Contact
109
-
110
- To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
@@ -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
@@ -12,6 +12,7 @@ module Minitest # :nodoc:
12
12
  opts[:top_count] = ENV["EVENT_PROF_TOP"].to_i if ENV["EVENT_PROF_TOP"]
13
13
  opts[:per_example] = true if ENV["EVENT_PROF_EXAMPLES"]
14
14
  opts[:fdoc] = true if ENV["FDOC"]
15
+ opts[:sample] = true if ENV["SAMPLE"] || ENV["SAMPLE_GROUPS"]
15
16
  end
16
17
  end
17
18
  end
@@ -39,5 +40,7 @@ module Minitest # :nodoc:
39
40
 
40
41
  reporter << TestProf::EventProfReporter.new(options[:io], options) if options[:event]
41
42
  reporter << TestProf::FactoryDoctorReporter.new(options[:io], options) if options[:fdoc]
43
+
44
+ ::TestProf::MinitestSample.call if options[:sample]
42
45
  end
43
46
  end
@@ -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,207 @@
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
+ @file = File.open(tmp_path, "w")
49
+ end
50
+
51
+ def start(_event, _id, payload)
52
+ sql = payload.fetch(:sql)
53
+ return if sql.match?(ANY_FIXTURE_IGNORE_RXP)
54
+
55
+ matches = sql.match(MODIFY_RXP)
56
+ return unless matches
57
+
58
+ reset_pk!(matches[2]) if /insert/i.match?(matches[1])
59
+ end
60
+
61
+ def finish(_event, _id, payload)
62
+ sql = payload.fetch(:sql)
63
+ return unless trackable_sql?(sql)
64
+
65
+ sql = payload[:binds].any? ? adapter.compile_sql(sql, quoted(payload[:binds])) : +sql
66
+
67
+ sql.tr!("\n", " ")
68
+
69
+ file.write(sql + ";\n")
70
+ end
71
+
72
+ def commit
73
+ file.close
74
+
75
+ FileUtils.mv(tmp_path, path)
76
+ end
77
+
78
+ private
79
+
80
+ attr_reader :file, :reset_pk, :adapter
81
+
82
+ def reset_pk!(table_name)
83
+ return if /sqlite_sequence/.match?(table_name)
84
+
85
+ return if reset_pk.include?(table_name)
86
+
87
+ adapter.reset_sequence!(table_name, AnyFixture.config.dump_sequence_random_start)
88
+ reset_pk << table_name
89
+ end
90
+
91
+ def trackable_sql?(sql)
92
+ return false if sql.match?(ANY_FIXTURE_IGNORE_RXP)
93
+
94
+ sql.match?(MODIFY_RXP) || sql.match?(ANY_FIXTURE_RXP) || sql.match?(AnyFixture.config.dump_matching_queries)
95
+ end
96
+
97
+ def quoted(val)
98
+ if val.is_a?(Array)
99
+ val.map { |v| quoted(v) }
100
+ elsif val.is_a?(ActiveRecord::Relation::QueryAttribute)
101
+ quoted(val.value_for_database)
102
+ else
103
+ ActiveRecord::Base.connection.quote(val)
104
+ end
105
+ end
106
+ end
107
+
108
+ attr_reader :name, :digest, :path, :subscriber, :success
109
+ alias success? success
110
+
111
+ def initialize(name, watch: [], cache_key: nil)
112
+ @name = name
113
+ @digest = [
114
+ Digest.call(*watch),
115
+ cache_key.to_digest
116
+ ].compact.join("-")
117
+
118
+ @path = build_path(name, digest)
119
+
120
+ @success = false
121
+
122
+ @adapter =
123
+ case ActiveRecord::Base.connection.adapter_name
124
+ when /sqlite/i
125
+ require "test_prof/any_fixture/dump/sqlite"
126
+ SQLite.new
127
+ when /postgresql/i
128
+ require "test_prof/any_fixture/dump/postgresql"
129
+ PostgreSQL.new
130
+ else
131
+ raise ArgumentError,
132
+ "Your current database adapter (#{ActiveRecord::Base.connection.adapter_name}) " \
133
+ "is currently not supported. So far, we only support SQLite and PostgreSQL"
134
+ end
135
+
136
+ @subscriber = Subscriber.new(path, adapter)
137
+ end
138
+
139
+ def exists?
140
+ File.exist?(path)
141
+ end
142
+
143
+ def force?
144
+ AnyFixture.config.force_matching_dumps.match?(name)
145
+ end
146
+
147
+ def load
148
+ return import_via_active_record unless AnyFixture.config.import_dump_via_cli?
149
+
150
+ adapter.import(path) || import_via_active_record
151
+ end
152
+
153
+ def commit!
154
+ subscriber.commit
155
+ end
156
+
157
+ def within_prepared_env(before: nil, after: nil, import: false)
158
+ run_before_callbacks(callback: before, dump: self, import: false)
159
+ yield.tap do
160
+ @success = true
161
+ end
162
+ ensure
163
+ run_after_callbacks(callback: after, dump: self, import: false)
164
+ end
165
+
166
+ private
167
+
168
+ attr_reader :adapter
169
+
170
+ def import_via_active_record
171
+ conn = ActiveRecord::Base.connection
172
+
173
+ File.open(path).each_line do |query|
174
+ next if query.empty?
175
+
176
+ conn.execute query
177
+ end
178
+ end
179
+
180
+ def build_path(name, digest)
181
+ dir = TestProf.artifact_path(
182
+ File.join(AnyFixture.config.dumps_dir)
183
+ )
184
+
185
+ FileUtils.mkdir_p(dir)
186
+
187
+ File.join(dir, "#{name}-#{digest}.sql")
188
+ end
189
+
190
+ def run_before_callbacks(callback:, **options)
191
+ # First, call config-defined setup callbacks
192
+ AnyFixture.config.before_dump.each { |clbk| clbk.call(**options) }
193
+ # Then, adapter-defined callbacks
194
+ adapter.setup_env unless options[:import]
195
+ # Finally, user-provided callback
196
+ callback&.call(**options)
197
+ end
198
+
199
+ def run_after_callbacks(callback:, **options)
200
+ # The order is vice versa to setup
201
+ callback&.call(**options)
202
+ adapter.teardown_env unless options[:import]
203
+ AnyFixture.config.after_dump.each { |clbk| clbk.call(**options) }
204
+ end
205
+ end
206
+ end
207
+ end