isolator 0.4.0 → 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f375e040a8810bf83286b3bd78172a5b3e17504c21dc71d74dcc09b6313427d
4
- data.tar.gz: faf57a3cc344a2fb889cc0ba87a1b47b33514330247b5d44574cf765e763c451
3
+ metadata.gz: 00d85d8a62a5a34bd39ef9295e1e8ad9c13599fc17648bbda7a5012f9a89ddfa
4
+ data.tar.gz: cf7a441154f63bc40258b4a78e57e3d3f46158b915167c1631d717530571dcd2
5
5
  SHA512:
6
- metadata.gz: 4136572ecead2ed8eaef576958c317ef2ff42d8502a9caa04c974ca1ce5e5661231c42dfa83039e81d6cca45e13926115709c61223d381e42e83bf24f8ca9395
7
- data.tar.gz: 42de6787f0c17d7b5da48be6b465479a37b4e20e7590cafaf261edf5f0ca29a9a341d451dfb6293aacb7e9958d0e47cf3aefcd53c77c900fe5ff3e04de15de6e
6
+ metadata.gz: a088b0f2e24c8afd8a2ec3fd65df4ef5e2d550f55c8376132955db8f870f68a28f021b8079024b413ef2692a474136f3d7da56c098a89d2e3b3aa52deb0ce334
7
+ data.tar.gz: d3667e26d54697e09eaefd7cdee7e9c5586e307c371ce41215d8eca88dc514c652079088bba2af3aeb654b9bfbdad3a3ab6080c5f20b47867291cf4734f16cda
@@ -2,6 +2,49 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.7.0 (2020-09-25)
6
+
7
+ - Add debug mode. ([@palkan][])
8
+
9
+ Use `ISOLATOR_DEBUG=true` to turn on debug mode, which prints some useful information: when a transaction is tracked,
10
+ thresholds are changed, etc.
11
+
12
+ - Track transactions for different connections independently. ([@mquan][], [@palkan][])
13
+
14
+ This, for example, makes Isolator compatible with Rails multi-database apps.
15
+
16
+ - Allow custom ignorer usage. ([@iiwo][])
17
+
18
+ - `Isolator.load_ignore_config` is deprecated in favor of `Isolator::Ignorer.prepare`. ([@iiwo][])
19
+
20
+ ## 0.6.2 (2020-03-20)
21
+
22
+ - Make Sniffer version requirement open-ended. ([@palkan][])
23
+
24
+ - **Support Ruby 2.5+** ([@palkan][])
25
+
26
+ ## 0.6.1 (2019-09-06)
27
+
28
+ - Fix Sniffer integration. ([@palkan][])
29
+
30
+ Fixes [#21](https://github.com/palkan/isolator/issues/21).
31
+
32
+ ## 0.6.0 (2019-04-12) 🚀
33
+
34
+ - Add support for exceptions message details. ([@palkan][])
35
+
36
+ Make it possible to provide more information about the cause of the failure
37
+ (for example, job class and arguments for background jobs, URL for HTTP).
38
+
39
+ - Change backtrace filtering behaviour. ([@palkan][])
40
+
41
+ The default behaviour is to take the top five lines.
42
+ You can customize it via `Isolator.config.backtrace_filter`.
43
+
44
+ ## 0.5.0 (2018-08-29)
45
+
46
+ - [PR [#19](https://github.com/palkan/isolator/pull/19)] Adding support for ruby version 2.2.2. ([@shivanshgaur][])
47
+
5
48
  ## 0.4.0 (2018-06-15)
6
49
 
7
50
  - [PR [#13](https://github.com/palkan/isolator/pull/13)] Allow load ignored offences from YML file using `load_ignore_config`. ([@DmitryTsepelev][])
@@ -47,3 +90,6 @@
47
90
  [@dsalahutdinov]: https://github.com/dsalahutdinov
48
91
  [@Envek]: https://github.com/Envek
49
92
  [@DmitryTsepelev]: https://github.com/DmitryTsepelev
93
+ [@shivanshgaur]: https://github.com/shivanshgaur
94
+ [@iiwo]: https://github.com/iiwo
95
+ [@mquan]: https://github.com/mquan
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018 Vladimir Dementyev
3
+ Copyright (c) 2018-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,6 +1,6 @@
1
1
  [![Cult Of Martians](http://cultofmartians.com/assets/badges/badge.svg)](http://cultofmartians.com/tasks/isolator.html)
2
2
  [![Gem Version](https://badge.fury.io/rb/isolator.svg)](https://badge.fury.io/rb/isolator)
3
- [![Build Status](https://travis-ci.org/palkan/isolator.svg?branch=master)](https://travis-ci.org/palkan/isolator)
3
+ ![Build](https://github.com/palkan/isolator/workflows/Build/badge.svg)
4
4
 
5
5
  # Isolator
6
6
 
@@ -79,6 +79,8 @@ However, there are some potential caveats:
79
79
 
80
80
  3) Isolator tries to detect the `test` environment and slightly change its behavior: first, it respect _transactional tests_; secondly, error raising is turned on by default (see [below](#configuration)).
81
81
 
82
+ 4) Experimental [multiple databases](https://guides.rubyonrails.org/active_record_multiple_databases.html) has been added in v0.7.0. Please, let us know if you encounter any issues.
83
+
82
84
  ### Configuration
83
85
 
84
86
  ```ruby
@@ -91,6 +93,14 @@ Isolator.configure do |config|
91
93
 
92
94
  # Send notifications to uniform_notifier
93
95
  config.send_notifications = false
96
+
97
+ # Customize backtrace filtering (provide a callable)
98
+ # By default, just takes the top-5 lines
99
+ config.backtrace_filter = ->(backtrace) { backtrace.take(5) }
100
+
101
+ # Define a custom ignorer class (must implement .prepare)
102
+ # uses a row number based list from the .isolator_todo.yml file
103
+ config.ignorer = Isolator::Ignorer
94
104
  end
95
105
  ```
96
106
 
@@ -106,7 +116,7 @@ Isolator relys on [uniform_notifier][] to send custom notifications.
106
116
  ### Supported ORMs
107
117
 
108
118
  - `ActiveRecord` >= 4.1
109
- - `ROM::SQL` (only if Active Support instrumentation extenstion is loaded)
119
+ - `ROM::SQL` (only if Active Support instrumentation extension is loaded)
110
120
 
111
121
  ### Adapters
112
122
 
@@ -163,7 +173,7 @@ All the exceptions raised in the listed lines will be ignored.
163
173
 
164
174
  ### Using with legacy Ruby codebases
165
175
 
166
- If you are not using Rails, you'll have to load ignores from file manually, using `Isolator#load_ignore_config`, for instance `Isolator.load_ignore_config("./config/.isolator_todo.yml")`
176
+ If you are not using Rails, you'll have to load ignores from file manually, using `Isolator::Ignorer.prepare(path:)`, for instance `Isolator::Ignorer.prepare(path: "./config/.isolator_todo.yml")`
167
177
 
168
178
  ## Custom Adapters
169
179
 
@@ -179,13 +189,24 @@ Suppose that you have a class `Danger` with a method `#explode`, which is not sa
179
189
  # the third one is a method name.
180
190
  Isolator.isolate :danger, Danger, :explode, options
181
191
 
182
- # NOTE: if you want to isolate a class method, use signleton_class instead
192
+ # NOTE: if you want to isolate a class method, use singleton_class instead
183
193
  Isolator.isolate :danger, Danger.singleton_class, :explode, options
184
194
  ```
185
195
 
186
196
  Possible `options` are:
187
197
  - `exception_class` – an exception class to raise in case of offense
188
198
  - `exception_message` – custom exception message (could be specified without a class)
199
+ - `details_message` – a block to generate additional exceptin message information:
200
+
201
+ ```ruby
202
+ Isolator.isolate :active_job,
203
+ target: ActiveJob::Base,
204
+ method_name: :enqueue,
205
+ exception_class: Isolator::BackgroundJobError,
206
+ details_message: ->(obj, _args) {
207
+ "#{obj.class.name}(#{obj.arguments})"
208
+ }
209
+ ```
189
210
 
190
211
  You can also add some callbacks to be run before and after the transaction:
191
212
 
@@ -195,10 +216,22 @@ Isolator.before_isolate do
195
216
  end
196
217
 
197
218
  Isolator.after_isolate do
198
- # right after the transaction has been committed/rollbacked
219
+ # right after the transaction has been committed/rolled back
199
220
  end
200
221
  ```
201
222
 
223
+ ## Troubleshooting
224
+
225
+ ### Verbose output
226
+
227
+ In most cases, turning on verbose output for Isolator helps to identify the issue. To do that, you can either specify `ISOLATOR_DEBUG=true` environment variable or set `Isolator.debug_enabled` manually.
228
+
229
+ ### Tests failing after upgrading to Rails 6.0.3 while using [Combustion](https://github.com/pat/combustion)
230
+
231
+ The reason is that Rails started using a [separate connection pool for advisory locks](https://github.com/rails/rails/pull/38235) since 6.0.3. Since Combustion usually applies migrations for every test run, this pool becomse visible to [test fixtures](https://github.com/rails/rails/blob/b738f1930f3c82f51741ef7241c1fee691d7deb2/activerecord/lib/active_record/test_fixtures.rb#L123-L127), which resulted in 2 transactional commits tracked by Isolator, which only expects one. That leads to false negatives.
232
+
233
+ To fix this disable migrations advisory locks by adding `advisory_locks: false` to your database configuration in `(spec|test)/internal/config/database.yml`.
234
+
202
235
  ## Contributing
203
236
 
204
237
  Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/isolator.
@@ -17,7 +17,25 @@ require "isolator/ext/thread_fetch"
17
17
  module Isolator
18
18
  using Isolator::ThreadFetch
19
19
 
20
+ class ThreadStateProxy
21
+ attr_reader :prefix
22
+
23
+ def initilize(prefix = "isolator_")
24
+ @prefix = prefix
25
+ end
26
+
27
+ def [](key)
28
+ Thread.current[:"#{prefix}#{key}"]
29
+ end
30
+
31
+ def []=(key, value)
32
+ Thread.current[:"#{prefix}#{key}"] = value
33
+ end
34
+ end
35
+
20
36
  class << self
37
+ attr_accessor :default_threshold, :default_connection_id
38
+
21
39
  def config
22
40
  @config ||= Configuration.new
23
41
  end
@@ -31,11 +49,11 @@ module Isolator
31
49
  end
32
50
 
33
51
  def enable!
34
- Thread.current[:isolator_disabled] = false
52
+ state[:disabled] = false
35
53
  end
36
54
 
37
55
  def disable!
38
- Thread.current[:isolator_disabled] = true
56
+ state[:disabled] = true
39
57
  end
40
58
 
41
59
  # Accepts block and disable Isolator within
@@ -64,32 +82,77 @@ module Isolator
64
82
  res
65
83
  end
66
84
 
67
- def transactions_threshold
68
- Thread.current.fetch(:isolator_threshold, 1)
85
+ def transactions_threshold=(val)
86
+ set_connection_threshold(val)
87
+ end
88
+
89
+ def transactions_threshold(connection_id = default_connection_id.call)
90
+ connection_threshold(connection_id)
69
91
  end
70
92
 
71
- def transactions_threshold=(val)
72
- Thread.current[:isolator_threshold] = val
93
+ def current_transactions(connection_id = default_connection_id.call)
94
+ state[:transactions]&.[](connection_id) || 0
73
95
  end
74
96
 
75
- def incr_transactions!
76
- Thread.current[:isolator_transactions] =
77
- Thread.current.fetch(:isolator_transactions, 0) + 1
78
- start! if Thread.current.fetch(:isolator_transactions) == transactions_threshold
97
+ def set_connection_threshold(val, connection_id = default_connection_id.call)
98
+ state[:thresholds] ||= Hash.new { |h, k| h[k] = Isolator.default_threshold }
99
+ state[:thresholds][connection_id] = val
100
+
101
+ debug!("Threshold value was changed for connection #{connection_id}: #{val}")
79
102
  end
80
103
 
81
- def decr_transactions!
82
- Thread.current[:isolator_transactions] =
83
- Thread.current.fetch(:isolator_transactions) - 1
84
- finish! if Thread.current.fetch(:isolator_transactions) == (transactions_threshold - 1)
104
+ def incr_thresholds!
105
+ self.default_threshold += 1
106
+ return unless state[:thresholds]
107
+
108
+ state[:thresholds].transform_values!(&:succ)
109
+
110
+ debug!("Thresholds were incremented")
111
+ end
112
+
113
+ def decr_thresholds!
114
+ self.default_threshold -= 1
115
+ return unless state[:thresholds]
116
+
117
+ state[:thresholds].transform_values!(&:pred)
118
+
119
+ debug!("Thresholds were decremented")
120
+ end
121
+
122
+ def incr_transactions!(connection_id = default_connection_id.call)
123
+ state[:transactions] ||= Hash.new { |h, k| h[k] = 0 }
124
+ state[:transactions][connection_id] += 1
125
+
126
+ # Workaround to track threshold changes made before opening a connection
127
+ pending_threshold = state[:thresholds]&.delete(0)
128
+ if pending_threshold
129
+ state[:thresholds][connection_id] = pending_threshold
130
+ end
131
+
132
+ debug!("Transaction opened for connection #{connection_id} (total: #{state[:transactions][connection_id]}, threshold: #{state[:thresholds]&.fetch(connection_id, default_threshold)})")
133
+
134
+ start! if current_transactions(connection_id) == connection_threshold(connection_id)
135
+ end
136
+
137
+ def decr_transactions!(connection_id = default_connection_id.call)
138
+ state[:transactions][connection_id] -= 1
139
+
140
+ finish! if current_transactions(connection_id) == (connection_threshold(connection_id) - 1)
141
+
142
+ state[:transactions].delete(connection_id) if state[:transactions][connection_id].zero?
143
+
144
+ debug!("Transaction closed for connection #{connection_id} (total: #{state[:transactions][connection_id]}, threshold: #{state[:thresholds]&.[](connection_id) || default_threshold})")
85
145
  end
86
146
 
87
147
  def clear_transactions!
88
- Thread.current[:isolator_transactions] = 0
148
+ state[:transactions]&.clear
89
149
  end
90
150
 
91
151
  def within_transaction?
92
- Thread.current.fetch(:isolator_transactions, 0) >= transactions_threshold
152
+ state[:transactions]&.each do |connection_id, transaction_count|
153
+ return true if transaction_count >= connection_threshold(connection_id)
154
+ end
155
+ false
93
156
  end
94
157
 
95
158
  def enabled?
@@ -97,17 +160,61 @@ module Isolator
97
160
  end
98
161
 
99
162
  def disabled?
100
- Thread.current[:isolator_disabled] == true
163
+ state[:disabled] == true
101
164
  end
102
165
 
103
166
  def adapters
104
167
  @adapters ||= Isolator::SimpleHashie.new
105
168
  end
106
169
 
170
+ def load_ignore_config(path)
171
+ warn "[DEPRECATION] `load_ignore_config` is deprecated. Please use `Isolator::Ignorer.prepare` instead."
172
+ Isolator::Ignorer.prepare(path: path)
173
+ end
174
+
107
175
  include Isolator::Isolate
108
176
  include Isolator::Callbacks
109
- include Isolator::Ignorer
177
+
178
+ attr_accessor :debug_enabled, :backtrace_cleaner, :backtrace_length
179
+
180
+ private
181
+
182
+ attr_accessor :state
183
+
184
+ def connection_threshold(connection_id)
185
+ state[:thresholds]&.[](connection_id) || default_threshold
186
+ end
187
+
188
+ def debug!(msg)
189
+ return unless debug_enabled
190
+ msg = "[ISOLATOR DEBUG] #{msg}"
191
+
192
+ if backtrace_cleaner && backtrace_length.positive?
193
+ source = extract_source_location(caller)
194
+
195
+ msg = "#{msg}\n ↳ #{source.join("\n")}" unless source.empty?
196
+ end
197
+
198
+ $stdout.puts(colorize_debug(msg))
199
+ end
200
+
201
+ def extract_source_location(locations)
202
+ backtrace_cleaner.call(locations.lazy)
203
+ .take(backtrace_length).to_a
204
+ end
205
+
206
+ def colorize_debug(msg)
207
+ return msg unless $stdout.tty?
208
+
209
+ "\u001b[31;1m#{msg}\u001b[0m"
210
+ end
110
211
  end
212
+
213
+ self.state = ThreadStateProxy.new
214
+ self.default_threshold = 1
215
+ self.default_connection_id = -> { ActiveRecord::Base.connected? ? ActiveRecord::Base.connection.object_id : 0 }
216
+ self.debug_enabled = ENV["ISOLATOR_DEBUG"] == "true"
217
+ self.backtrace_length = ENV.fetch("ISOLATOR_BACKTRACE_LENGTH", 1).to_i
111
218
  end
112
219
 
113
220
  require "isolator/orm_adapters"
@@ -11,6 +11,7 @@ module Isolator
11
11
 
12
12
  self.exception_class = options[:exception_class] if options.key?(:exception_class)
13
13
  self.exception_message = options[:exception_message] if options.key?(:exception_message)
14
+ self.details_message = options[:details_message] if options.key?(:details_message)
14
15
  end
15
16
 
16
17
  add_patch_method(adapter, target, method_name) if
@@ -21,7 +22,7 @@ module Isolator
21
22
  def self.add_patch_method(adapter, base, method_name)
22
23
  mod = Module.new do
23
24
  define_method method_name do |*args, &block|
24
- adapter.notify(caller, *args)
25
+ adapter.notify(caller, self, *args)
25
26
  super(*args, &block)
26
27
  end
27
28
  end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Isolator.isolate :active_job,
4
- target: ActiveJob::Base,
5
- method_name: :enqueue,
6
- exception_class: Isolator::BackgroundJobError
4
+ target: ActiveJob::Base,
5
+ method_name: :enqueue,
6
+ exception_class: Isolator::BackgroundJobError,
7
+ details_message: ->(obj, _args) {
8
+ "#{obj.class.name}" \
9
+ "#{obj.arguments.any? ? " (#{obj.arguments.join(", ")})" : ""}"
10
+ }
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Isolator.isolate :resque,
4
- target: Resque.singleton_class,
5
- method_name: :enqueue,
6
- exception_class: Isolator::BackgroundJobError
4
+ target: Resque.singleton_class,
5
+ method_name: :enqueue,
6
+ exception_class: Isolator::BackgroundJobError,
7
+ details_message: ->(_obj, args) {
8
+ args.join(", ")
9
+ }
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Isolator.isolate :resque_scheduler,
4
- target: Resque.singleton_class,
5
- method_name: :enqueue_at,
6
- exception_class: Isolator::BackgroundJobError
4
+ target: Resque.singleton_class,
5
+ method_name: :enqueue_at,
6
+ exception_class: Isolator::BackgroundJobError,
7
+ details_message: ->(_obj, (ts, *args)) {
8
+ "#{args.join(", ")} (at #{ts})"
9
+ }
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Isolator.isolate :sidekiq,
4
- target: Sidekiq::Client,
5
- method_name: :raw_push,
6
- exception_class: Isolator::BackgroundJobError
4
+ target: Sidekiq::Client,
5
+ method_name: :raw_push,
6
+ exception_class: Isolator::BackgroundJobError,
7
+ details_message: ->(_obj, args) {
8
+ args.first.map do |job|
9
+ "#{job["class"]} (#{job["args"]})"
10
+ end.join("\n")
11
+ }
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Isolator.isolate :sucker_punch,
4
- target: SuckerPunch::Queue.singleton_class,
5
- method_name: :find_or_create,
6
- exception_class: Isolator::BackgroundJobError
4
+ target: SuckerPunch::Queue.singleton_class,
5
+ method_name: :find_or_create,
6
+ exception_class: Isolator::BackgroundJobError,
7
+ details_message: ->(_obj, args) {
8
+ args.compact.join(", ")
9
+ }
@@ -4,7 +4,7 @@ module Isolator
4
4
  module Adapters
5
5
  # Used as a "template" for adapters
6
6
  module Base
7
- attr_accessor :exception_class, :exception_message
7
+ attr_accessor :exception_class, :exception_message, :details_message
8
8
 
9
9
  def disable!
10
10
  @disabled = true
@@ -22,17 +22,17 @@ module Isolator
22
22
  @disabled == true
23
23
  end
24
24
 
25
- def notify(backtrace, *args)
25
+ def notify(backtrace, obj, *args)
26
26
  return unless notify?(*args)
27
- Isolator.notify(exception: build_exception, backtrace: backtrace)
27
+ Isolator.notify(exception: build_exception(obj, args), backtrace: backtrace)
28
28
  end
29
29
 
30
30
  def notify?(*args)
31
31
  enabled? && Isolator.enabled? && Isolator.within_transaction? && !ignored?(*args)
32
32
  end
33
33
 
34
- def ignore_if
35
- ignores << Proc.new
34
+ def ignore_if(&block)
35
+ ignores << block
36
36
  end
37
37
 
38
38
  def ignores
@@ -45,9 +45,10 @@ module Isolator
45
45
 
46
46
  private
47
47
 
48
- def build_exception
48
+ def build_exception(obj, args)
49
49
  klass = exception_class || Isolator::UnsafeOperationError
50
- klass.new(exception_message)
50
+ details = details_message.call(obj, args) if details_message
51
+ klass.new(exception_message, details: details)
51
52
  end
52
53
  end
53
54
  end
@@ -7,7 +7,11 @@ Sniffer::Config.defaults["logger"] = nil
7
7
 
8
8
  Isolator.isolate :http, target: Sniffer.singleton_class,
9
9
  method_name: :store,
10
- exception_class: Isolator::HTTPError
10
+ exception_class: Isolator::HTTPError,
11
+ details_message: ->(_obj, args) {
12
+ req = args.first.request
13
+ "#{req.method} #{req.host}:#{req.port}#{req.query}"
14
+ }
11
15
 
12
16
  Isolator.before_isolate do
13
17
  next if Isolator.adapters.http.disabled?
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- adapter = Isolator.isolate :webmock, exception_class: Isolator::HTTPError
3
+ adapter = Isolator.isolate :webmock,
4
+ exception_class: Isolator::HTTPError,
5
+ details_message: ->(obj, _args) {
6
+ "#{obj.method.to_s.upcase} #{obj.uri}"
7
+ }
4
8
 
5
9
  WebMock.after_request do |*args|
6
10
  adapter.notify(caller, *args)
@@ -1,4 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Isolator.isolate :mailer, target: Mail::Message, method_name: :deliver,
4
- exception_class: Isolator::MailerError
3
+ Isolator.isolate :mailer, target: Mail::Message,
4
+ method_name: :deliver,
5
+ exception_class: Isolator::MailerError,
6
+ details_message: ->(obj, _args) {
7
+ "From: #{obj.from}\n" \
8
+ "To: #{obj.to}\n" \
9
+ "Subject: #{obj.subject}"
10
+ }
@@ -5,19 +5,27 @@ module Isolator
5
5
  #
6
6
  # - `raise_exceptions` - whether to raise an exception in case of offense;
7
7
  # defaults to true in test env and false otherwise.
8
- # NOTE: env is infered from RACK_ENV and RAILS_ENV.
8
+ # NOTE: env is inferred from RACK_ENV and RAILS_ENV.
9
9
  #
10
10
  # - `logger` - logger instance (nil by default)
11
11
  #
12
12
  # - `send_notifications` - whether to send notifications (through uniform_notifier);
13
- # defauls to false
13
+ # defaults to false
14
+ #
15
+ # - `backtrace_filter` - define a custom backtrace filtering (provide a callable)
16
+ #
17
+ # - `ignorer` - define a custom ignorer (must implement .prepare)
18
+ #
14
19
  class Configuration
15
- attr_accessor :raise_exceptions, :logger, :send_notifications
20
+ attr_accessor :raise_exceptions, :logger, :send_notifications,
21
+ :backtrace_filter, :ignorer
16
22
 
17
23
  def initialize
18
24
  @logger = nil
19
25
  @raise_exceptions = test_env?
20
26
  @send_notifications = false
27
+ @backtrace_filter = ->(backtrace) { backtrace.take(5) }
28
+ @ignorer = Isolator::Ignorer
21
29
  end
22
30
 
23
31
  alias raise_exceptions? raise_exceptions
@@ -4,9 +4,9 @@ module Isolator # :nodoc: all
4
4
  class UnsafeOperationError < StandardError
5
5
  MESSAGE = "You are trying to do unsafe operation inside db transaction"
6
6
 
7
- def initialize(msg = nil)
7
+ def initialize(msg = nil, details: nil)
8
8
  msg ||= self.class::MESSAGE
9
- super
9
+ super(details ? "#{msg}\nDetails: #{details}" : msg)
10
10
  end
11
11
  end
12
12
 
@@ -1,39 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Isolator
4
- # Add .load_ignore_config function for ignoring patterns from file
5
- module Ignorer
6
- def load_ignore_config(path)
7
- return unless File.exist?(path)
4
+ # Handle ignoring isolator errors using a yml file
5
+ class Ignorer
6
+ TODO_PATH = ".isolator_todo.yml"
8
7
 
9
- todos = YAML.load_file(path)
8
+ class << self
9
+ def prepare(path: TODO_PATH, regex_string: "^.*(#ignores#):.*$")
10
+ return unless File.exist?(path)
10
11
 
11
- adapters.each do |id, adapter|
12
- ignored_paths = todos.fetch(id, [])
13
- configure_adapter(adapter, ignored_paths)
12
+ todos = YAML.load_file(path)
13
+
14
+ Isolator.adapters.each do |id, adapter|
15
+ ignored_paths = todos.fetch(id, [])
16
+ AdapterIgnore.new(adapter: adapter, ignored_paths: ignored_paths, regex_string: regex_string).prepare
17
+ end
14
18
  end
15
19
  end
16
20
 
17
21
  private
18
22
 
19
- def configure_adapter(adapter, ignored_paths)
20
- ignores = build_ignore_list(ignored_paths)
21
- return if ignores.blank?
23
+ class AdapterIgnore
24
+ def initialize(adapter:, ignored_paths:, regex_string:)
25
+ self.adapter = adapter
26
+ self.ignored_paths = ignored_paths
27
+ self.regex_string = regex_string
28
+ end
29
+
30
+ def prepare
31
+ return if ignores.blank?
22
32
 
23
- regex = Regexp.new("^.*(#{ignores.join('|')}):.*$")
24
- adapter.ignore_if { caller.any? { |row| regex =~ row } }
25
- end
33
+ adapter.ignore_if { caller.any? { |row| regex =~ row } }
34
+ end
26
35
 
27
- def build_ignore_list(ignored_paths)
28
- ignored_paths.each_with_object([]) do |path, result|
29
- ignored_files = Dir[path]
36
+ private
30
37
 
31
- if ignored_files.blank?
32
- result << path.to_s
33
- else
34
- result.concat(ignored_files)
38
+ attr_accessor :adapter, :ignored_paths, :regex_string
39
+
40
+ def ignores
41
+ return @ignores if defined? @ignores
42
+
43
+ @ignores = ignored_paths.each_with_object([]) do |path, result|
44
+ ignored_files = Dir[path]
45
+
46
+ if ignored_files.blank?
47
+ result << path.to_s
48
+ else
49
+ result.concat(ignored_files)
50
+ end
35
51
  end
36
52
  end
53
+
54
+ def regex
55
+ Regexp.new(regex_string.gsub("#ignores#", ignores.join("|")))
56
+ end
37
57
  end
38
58
  end
39
59
  end
@@ -5,7 +5,7 @@ module Isolator
5
5
  module Isolate
6
6
  def isolate(id, **options)
7
7
  raise "Adapter already registered: #{id}" if Isolator.adapters.key?(id.to_s)
8
- adapter = AdapterBuilder.call(options)
8
+ adapter = AdapterBuilder.call(**options)
9
9
  Isolator.adapters[id.to_s] = adapter
10
10
  end
11
11
  end
@@ -48,7 +48,7 @@ module Isolator
48
48
  end
49
49
 
50
50
  def filtered_backtrace
51
- backtrace.reject { |line| line =~ /\/(gems|ruby)/ }.take_while { |line| line !~ /ruby/ }
51
+ Isolator.config.backtrace_filter.call(backtrace)
52
52
  end
53
53
 
54
54
  def uniform_notifier_loaded?
@@ -9,8 +9,9 @@ module Isolator
9
9
 
10
10
  def self.subscribe!(event)
11
11
  ::ActiveSupport::Notifications.subscribe(event) do |_name, _start, _finish, _id, query|
12
- Isolator.incr_transactions! if query[:sql] =~ START_PATTERN
13
- Isolator.decr_transactions! if query[:sql] =~ FINISH_PATTERN
12
+ connection_id = query[:connection_id] || query[:connection]&.object_id || 0
13
+ Isolator.incr_transactions!(connection_id) if START_PATTERN.match?(query[:sql])
14
+ Isolator.decr_transactions!(connection_id) if FINISH_PATTERN.match?(query[:sql])
14
15
  end
15
16
  end
16
17
  end
@@ -2,12 +2,20 @@
2
2
 
3
3
  module Isolator
4
4
  class Railtie < ::Rails::Railtie # :nodoc:
5
+ initializer "isolator.backtrace_cleaner" do
6
+ ActiveSupport.on_load(:active_record) do
7
+ Isolator.backtrace_cleaner = lambda do |locations|
8
+ ::Rails.backtrace_cleaner.clean(locations)
9
+ end
10
+ end
11
+ end
12
+
5
13
  config.after_initialize do
6
14
  # Forec load adapters after application initialization
7
15
  # (when all deps are likely to be loaded).
8
16
  load File.join(__dir__, "adapters.rb")
9
17
 
10
- Isolator.load_ignore_config(Rails.root.join(".isolator_todo.yml"))
18
+ Isolator.config.ignorer&.prepare
11
19
 
12
20
  next unless Rails.env.test?
13
21
 
@@ -18,14 +26,12 @@ module Isolator
18
26
  super
19
27
  return unless run_in_transaction?
20
28
 
21
- open_count = ActiveRecord::Base.connection.open_transactions
22
- Isolator.transactions_threshold += open_count
29
+ Isolator.incr_thresholds!
23
30
  end
24
31
 
25
32
  def teardown_fixtures(*)
26
33
  if run_in_transaction?
27
- open_count = ActiveRecord::Base.connection.open_transactions
28
- Isolator.transactions_threshold -= open_count
34
+ Isolator.decr_thresholds!
29
35
  end
30
36
  super
31
37
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Isolator
4
- VERSION = "0.4.0"
4
+ VERSION = "0.7.0"
5
5
  end
metadata CHANGED
@@ -1,57 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: isolator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-15 00:00:00.000000000 Z
11
+ date: 2020-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sniffer
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 0.3.1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.3.1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1.14'
33
+ version: '1.16'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '1.14'
40
+ version: '1.16'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '10.0'
47
+ version: '13.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '10.0'
54
+ version: '13.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rspec
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -70,14 +70,14 @@ dependencies:
70
70
  name: rspec-rails
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - "~>"
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '3.0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - "~>"
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '3.0'
83
83
  - !ruby/object:Gem::Dependency
@@ -95,91 +95,91 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: 5.10.0
97
97
  - !ruby/object:Gem::Dependency
98
- name: rubocop
98
+ name: sidekiq
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.56.0
103
+ version: '5.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 0.56.0
110
+ version: '5.0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: rubocop-md
112
+ name: webmock
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '0.2'
117
+ version: '3.1'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '0.2'
124
+ version: '3.1'
125
125
  - !ruby/object:Gem::Dependency
126
- name: uniform_notifier
126
+ name: test_after_commit
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '1.11'
131
+ version: '1.1'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '1.11'
138
+ version: '1.1'
139
139
  - !ruby/object:Gem::Dependency
140
- name: sidekiq
140
+ name: resque
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - "~>"
143
+ - - ">="
144
144
  - !ruby/object:Gem::Version
145
- version: '5.0'
145
+ version: '0'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - "~>"
150
+ - - ">="
151
151
  - !ruby/object:Gem::Version
152
- version: '5.0'
152
+ version: '0'
153
153
  - !ruby/object:Gem::Dependency
154
- name: webmock
154
+ name: fakeredis
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
- - - "~>"
157
+ - - ">="
158
158
  - !ruby/object:Gem::Version
159
- version: '3.1'
159
+ version: '0'
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
- - - "~>"
164
+ - - ">="
165
165
  - !ruby/object:Gem::Version
166
- version: '3.1'
166
+ version: '0'
167
167
  - !ruby/object:Gem::Dependency
168
- name: test_after_commit
168
+ name: resque-scheduler
169
169
  requirement: !ruby/object:Gem::Requirement
170
170
  requirements:
171
- - - "~>"
171
+ - - ">="
172
172
  - !ruby/object:Gem::Version
173
- version: '1.1'
173
+ version: '0'
174
174
  type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
- - - "~>"
178
+ - - ">="
179
179
  - !ruby/object:Gem::Version
180
- version: '1.1'
180
+ version: '0'
181
181
  - !ruby/object:Gem::Dependency
182
- name: resque
182
+ name: sucker_punch
183
183
  requirement: !ruby/object:Gem::Requirement
184
184
  requirements:
185
185
  - - ">="
@@ -193,7 +193,7 @@ dependencies:
193
193
  - !ruby/object:Gem::Version
194
194
  version: '0'
195
195
  - !ruby/object:Gem::Dependency
196
- name: fakeredis
196
+ name: database_cleaner
197
197
  requirement: !ruby/object:Gem::Requirement
198
198
  requirements:
199
199
  - - ">="
@@ -207,7 +207,7 @@ dependencies:
207
207
  - !ruby/object:Gem::Version
208
208
  version: '0'
209
209
  - !ruby/object:Gem::Dependency
210
- name: resque-scheduler
210
+ name: database_cleaner-active_record
211
211
  requirement: !ruby/object:Gem::Requirement
212
212
  requirements:
213
213
  - - ">="
@@ -221,7 +221,7 @@ dependencies:
221
221
  - !ruby/object:Gem::Version
222
222
  version: '0'
223
223
  - !ruby/object:Gem::Dependency
224
- name: sucker_punch
224
+ name: after_commit_everywhere
225
225
  requirement: !ruby/object:Gem::Requirement
226
226
  requirements:
227
227
  - - ">="
@@ -235,7 +235,7 @@ dependencies:
235
235
  - !ruby/object:Gem::Version
236
236
  version: '0'
237
237
  - !ruby/object:Gem::Dependency
238
- name: database_cleaner
238
+ name: uniform_notifier
239
239
  requirement: !ruby/object:Gem::Requirement
240
240
  requirements:
241
241
  - - ">="
@@ -255,20 +255,9 @@ executables: []
255
255
  extensions: []
256
256
  extra_rdoc_files: []
257
257
  files:
258
- - ".gitignore"
259
- - ".rubocop.yml"
260
- - ".travis.yml"
261
258
  - CHANGELOG.md
262
- - Gemfile
263
259
  - LICENSE.txt
264
260
  - README.md
265
- - Rakefile
266
- - bin/console
267
- - bin/setup
268
- - gemfiles/activerecord42.gemfile
269
- - gemfiles/jruby.gemfile
270
- - gemfiles/railsmaster.gemfile
271
- - isolator.gemspec
272
261
  - lib/isolator.rb
273
262
  - lib/isolator/adapter_builder.rb
274
263
  - lib/isolator/adapters.rb
@@ -303,7 +292,12 @@ files:
303
292
  homepage: https://github.com/palkan/isolator
304
293
  licenses:
305
294
  - MIT
306
- metadata: {}
295
+ metadata:
296
+ bug_tracker_uri: http://github.com/palkan/isolator/issues
297
+ changelog_uri: https://github.com/palkan/isolator/blob/master/CHANGELOG.md
298
+ documentation_uri: http://github.com/palkan/isolator
299
+ homepage_uri: http://github.com/palkan/isolator
300
+ source_code_uri: http://github.com/palkan/isolator
307
301
  post_install_message:
308
302
  rdoc_options: []
309
303
  require_paths:
@@ -312,15 +306,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
312
306
  requirements:
313
307
  - - ">="
314
308
  - !ruby/object:Gem::Version
315
- version: 2.3.0
309
+ version: 2.5.0
316
310
  required_rubygems_version: !ruby/object:Gem::Requirement
317
311
  requirements:
318
312
  - - ">="
319
313
  - !ruby/object:Gem::Version
320
314
  version: '0'
321
315
  requirements: []
322
- rubyforge_project:
323
- rubygems_version: 2.7.6
316
+ rubygems_version: 3.0.6
324
317
  signing_key:
325
318
  specification_version: 4
326
319
  summary: Detect non-atomic interactions within DB transactions
data/.gitignore DELETED
@@ -1,13 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
- *.gem
11
- gemfiles/*.lock
12
- Gemfile.local
13
- .rspec_status
@@ -1,62 +0,0 @@
1
- require:
2
- - rubocop-md
3
-
4
- AllCops:
5
- Include:
6
- - 'lib/**/*.rb'
7
- - 'lib/**/*.rake'
8
- - 'spec/**/*.rb'
9
- Exclude:
10
- - 'bin/**/*'
11
- - 'gemfiles/**/*'
12
- - 'spec/dummy/**/*'
13
- - 'vendor/**/*'
14
- - 'tmp/**/*'
15
- - 'Rakefile'
16
- - 'Gemfile'
17
- - '*.gemspec'
18
- DisplayCopNames: true
19
- StyleGuideCopsOnly: false
20
- TargetRubyVersion: 2.3
21
-
22
- Rails:
23
- Enabled: false
24
-
25
- Bundler/OrderedGems:
26
- Enabled: false
27
-
28
- Naming/UncommunicativeMethodParamName:
29
- Enabled: false
30
-
31
- Style/SymbolArray:
32
- Enabled: false
33
-
34
- Style/Documentation:
35
- Exclude:
36
- - 'spec/**/*.rb'
37
-
38
- Style/StringLiterals:
39
- EnforcedStyle: double_quotes
40
-
41
- Style/RegexpLiteral:
42
- Enabled: false
43
-
44
- Style/BlockDelimiters:
45
- Exclude:
46
- - 'spec/**/*.rb'
47
-
48
- Style/NumericPredicate:
49
- Enabled: false
50
-
51
- Layout/SpaceInsideStringInterpolation:
52
- EnforcedStyle: no_space
53
-
54
- Lint/AmbiguousRegexpLiteral:
55
- Enabled: false
56
-
57
- Metrics/LineLength:
58
- Max: 100
59
-
60
- Metrics/BlockLength:
61
- Exclude:
62
- - 'spec/**/*.rb'
@@ -1,26 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.5.0
5
-
6
- notifications:
7
- email: false
8
-
9
- matrix:
10
- fast_finish: true
11
- include:
12
- - rvm: ruby-head
13
- gemfile: gemfiles/railsmaster.gemfile
14
- - rvm: jruby-9.1.0.0
15
- gemfile: gemfiles/jruby.gemfile
16
- - rvm: 2.5.0
17
- gemfile: Gemfile
18
- - rvm: 2.4.3
19
- gemfile: Gemfile
20
- - rvm: 2.3.1
21
- gemfile: gemfiles/activerecord42.gemfile
22
- allow_failures:
23
- - rvm: ruby-head
24
- gemfile: gemfiles/railsmaster.gemfile
25
- - rvm: jruby-9.1.0.0
26
- gemfile: gemfiles/jruby.gemfile
data/Gemfile DELETED
@@ -1,18 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
-
5
- # Specify your gem's dependencies in isolator.gemspec
6
- gemspec
7
-
8
- gem "pry-byebug"
9
-
10
- gem "sqlite3"
11
-
12
- local_gemfile = File.join(__dir__, "Gemfile.local")
13
-
14
- if File.exist?(local_gemfile)
15
- eval(File.read(local_gemfile)) # rubocop:disable Security/Eval
16
- else
17
- gem "rails", "~> 5.0"
18
- end
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
- require "rubocop/rake_task"
4
-
5
- RSpec::Core::RakeTask.new(:spec)
6
- RuboCop::RakeTask.new
7
-
8
- task default: [:rubocop, :spec]
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "isolator"
5
-
6
- require "pry"
7
- Pry.start
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
@@ -1,6 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- gem "rails", "~> 4.2"
4
- gem "sqlite3"
5
-
6
- gemspec path: ".."
@@ -1,7 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- gem "activerecord-jdbcsqlite3-adapter", "~> 50.0"
4
- gem "jdbc-sqlite3"
5
- gem "rails", "~> 5.0"
6
-
7
- gemspec path: ".."
@@ -1,7 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- gem "arel", github: "rails/arel"
4
- gem "rails", github: "rails/rails"
5
- gem "sqlite3"
6
-
7
- gemspec path: ".."
@@ -1,42 +0,0 @@
1
- lib = File.expand_path("../lib", __FILE__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require "isolator/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "isolator"
7
- spec.version = Isolator::VERSION
8
- spec.authors = ["Vladimir Dementyev"]
9
- spec.email = ["dementiev.vm@gmail.com"]
10
-
11
- spec.summary = "Detect non-atomic interactions within DB transactions"
12
- spec.description = "Detect non-atomic interactions within DB transactions"
13
- spec.homepage = "https://github.com/palkan/isolator"
14
- spec.license = "MIT"
15
-
16
- spec.required_ruby_version = ">= 2.3.0"
17
-
18
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
- f.match(%r{^(test|spec|features)/})
20
- end
21
- spec.require_paths = ["lib"]
22
-
23
- spec.add_runtime_dependency "sniffer", "~> 0.3.1"
24
-
25
- spec.add_development_dependency "bundler", "~> 1.14"
26
- spec.add_development_dependency "rake", "~> 10.0"
27
- spec.add_development_dependency "rspec", "~> 3.0"
28
- spec.add_development_dependency "rspec-rails", "~> 3.0"
29
- spec.add_development_dependency "minitest", "~> 5.10.0"
30
- spec.add_development_dependency "rubocop", "~> 0.56.0"
31
- spec.add_development_dependency "rubocop-md", "~> 0.2"
32
-
33
- spec.add_development_dependency "uniform_notifier", "~> 1.11"
34
- spec.add_development_dependency "sidekiq", "~> 5.0"
35
- spec.add_development_dependency "webmock", "~> 3.1"
36
- spec.add_development_dependency "test_after_commit", "~> 1.1"
37
- spec.add_development_dependency "resque"
38
- spec.add_development_dependency "fakeredis"
39
- spec.add_development_dependency "resque-scheduler"
40
- spec.add_development_dependency "sucker_punch"
41
- spec.add_development_dependency "database_cleaner"
42
- end