isolator 0.4.0 → 0.7.0

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: 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