isolator 0.1.0.pre2 → 0.1.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: d70c61a526af789744667a5ef87f3b396e261102c414f004c874278a3a7af7cd
4
- data.tar.gz: 75432a1b9d3ad708269b55cfb675168abb8b2b83efabcb19a709f7da19ade6a8
3
+ metadata.gz: a15f425ab4ff004a59412c6c5f74da93ac998f90c50d5131ed244baa3b631174
4
+ data.tar.gz: 511c6210a02513d4c922dc7b00b89ea887be9601b9cf2892b913fc59e81ff74a
5
5
  SHA512:
6
- metadata.gz: 77d6b9bd87e603fd10415e234421ec60852228e50217f2ed37dac5aa837500fbfc6febaaa2aa8442375f6ca52a4e68474eb9d381ea99d19222ae1810eff03efa
7
- data.tar.gz: '0782abd5aede38aee2f303738629cc0c4fd4487163555691ad37622f673aa57b99b12ea1112bb609ab10225cf0e027237dac12c60bc213d6fcfbd46076c4f624'
6
+ metadata.gz: d6f2b7ca96011162f5436593bb42b60da26d720cfef028c7edee29abc703fb31a67d69d270c276a7c156265fb00e6f828b752f6957bec54f72440fafd762348d
7
+ data.tar.gz: c4ee00bd87e118a2d7c66ae90854f5cd3fc39a448cb30bb1a49aabd2eb11a75dc1579b47d9f95a11ccaabf37cc341a39b45906cd45d77d31159c5329d9e146af
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.1.0 (2018-02-19)
6
+
7
+ - Add `test_after_commit` patch. ([@palkan][])
8
+
9
+ - [PR [#7](https://github.com/palkan/isolator/pull/7)] Add WebMock adapter. ([@palkan][])
10
+
11
+ - Add `ignore_if` modifier to adapter. ([@palkan][])
12
+
5
13
  - [PR [#5](https://github.com/palkan/isolator/pull/5)] Add `mail` adapter. ([@alexshgov][])
6
14
 
7
15
  - Initial version. ([@palkan][], [@TheSmartnik][], [@alexshgov][])
data/README.md CHANGED
@@ -109,6 +109,7 @@ Isolator has a bunch of built-in adapters:
109
109
  - `:active_job`
110
110
  - `:sidekiq`
111
111
  - `:mailer`
112
+ - `:webmock` – track mocked HTTP requests (unseen by Sniffer) in tests
112
113
 
113
114
  You can dynamically enable/disable adapters, e.g.:
114
115
 
@@ -121,6 +122,20 @@ Isolator.adapters.http.disable!
121
122
  Isolator.adapters.http.enable!
122
123
  ```
123
124
 
125
+ ### Ignore Offenses
126
+
127
+ Since Isolator adapter is just a wrapper over original code, it may lead to false positives when there is another library patching the same behaviour. In that case you might want to ignore some offenses.
128
+
129
+ Consider an example: we use Sidekiq along with [`sidekiq-postpone`](https://github.com/marshall-lee/sidekiq-postpone)–gem that patches `Sidekiq::Client#raw_push` and allows you to postpone jobs enqueueing (e.g. to enqueue everything when a transaction is commited–we don't want to raise exceptions in such situation).
130
+
131
+ To ignore offenses when `sidekiq-postpone` is active, you can add an ignore `proc`:
132
+
133
+ ```ruby
134
+ Isolator.adapters.sidekiq.ignore_if { Thread.current[:sidekiq_postpone] }
135
+ ```
136
+
137
+ You can add as many _ignores_ as you want, the offense is registered iff all of them return false.
138
+
124
139
  ## Custom Adapters
125
140
 
126
141
  An adapter is just a combination of a _method wrapper_ and lifecycle hooks.
data/isolator.gemspec CHANGED
@@ -32,4 +32,6 @@ Gem::Specification.new do |spec|
32
32
 
33
33
  spec.add_development_dependency "uniform_notifier", "~> 1.11"
34
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"
35
37
  end
data/lib/isolator.rb CHANGED
@@ -37,6 +37,32 @@ module Isolator
37
37
  Thread.current[:isolator_disabled] = true
38
38
  end
39
39
 
40
+ # Accepts block and disable Isolator within
41
+ def disable
42
+ return yield if disabled?
43
+ res = nil
44
+ begin
45
+ disable!
46
+ res = yield
47
+ ensure
48
+ enable!
49
+ end
50
+ res
51
+ end
52
+
53
+ # Accepts block and enable Isolator within
54
+ def enable
55
+ return yield if enabled?
56
+ res = nil
57
+ begin
58
+ enable!
59
+ res = yield
60
+ ensure
61
+ disable!
62
+ end
63
+ res
64
+ end
65
+
40
66
  def transactions_threshold
41
67
  Thread.current.fetch(:isolator_threshold)
42
68
  end
@@ -46,14 +72,12 @@ module Isolator
46
72
  end
47
73
 
48
74
  def incr_transactions!
49
- return unless enabled?
50
75
  Thread.current[:isolator_transactions] =
51
76
  Thread.current.fetch(:isolator_transactions, 0) + 1
52
77
  start! if Thread.current.fetch(:isolator_transactions) == transactions_threshold
53
78
  end
54
79
 
55
80
  def decr_transactions!
56
- return unless enabled?
57
81
  Thread.current[:isolator_transactions] =
58
82
  Thread.current.fetch(:isolator_transactions) - 1
59
83
  finish! if Thread.current.fetch(:isolator_transactions) == (transactions_threshold - 1)
@@ -68,7 +92,11 @@ module Isolator
68
92
  end
69
93
 
70
94
  def enabled?
71
- Thread.current[:isolator_disabled] != true
95
+ !disabled?
96
+ end
97
+
98
+ def disabled?
99
+ Thread.current[:isolator_disabled] == true
72
100
  end
73
101
 
74
102
  def adapters
@@ -84,10 +112,5 @@ end
84
112
 
85
113
  require "isolator/orm_adapters"
86
114
 
87
- # Load adapters after application initialization
88
- # (when all deps are likely loaded).
89
- if defined?(Rails)
90
- require "isolator/railtie"
91
- else
92
- require "isolator/adapters"
93
- end
115
+ require "isolator/adapters"
116
+ require "isolator/railtie" if defined?(Rails)
@@ -5,7 +5,7 @@ require "isolator/adapters/base"
5
5
  module Isolator
6
6
  # Builds adapter from provided params
7
7
  module AdapterBuilder
8
- def self.call(target, method_name, **options)
8
+ def self.call(target: nil, method_name: nil, **options)
9
9
  adapter = Module.new do
10
10
  extend Isolator::Adapters::Base
11
11
 
@@ -13,14 +13,15 @@ module Isolator
13
13
  self.exception_message = options[:exception_message] if options.key?(:exception_message)
14
14
  end
15
15
 
16
- add_patch_method adapter, target, method_name
16
+ add_patch_method(adapter, target, method_name) if
17
+ target && method_name
17
18
  adapter
18
19
  end
19
20
 
20
21
  def self.add_patch_method(adapter, base, method_name)
21
22
  mod = Module.new do
22
23
  define_method method_name do |*args, &block|
23
- adapter.notify(caller) if adapter.notify_isolator?
24
+ adapter.notify(caller, *args)
24
25
  super(*args, &block)
25
26
  end
26
27
  end
@@ -3,3 +3,4 @@
3
3
  require "isolator/adapters/http"
4
4
  require "isolator/adapters/background_jobs"
5
5
  require "isolator/adapters/mailers"
6
+ require "isolator/adapters/after_commit" if defined?(::TestAfterCommit)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(Module.new do
4
+ def test_commit_records
5
+ ::Isolator.disable { super }
6
+ end
7
+ end)
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Isolator.isolate :active_job, ActiveJob::Base,
4
- :enqueue, exception_class: Isolator::BackgroundJobError
3
+ Isolator.isolate :active_job,
4
+ target: ActiveJob::Base,
5
+ method_name: :enqueue,
6
+ exception_class: Isolator::BackgroundJobError
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Isolator.isolate :sidekiq, Sidekiq::Client,
4
- :push, exception_class: Isolator::BackgroundJobError
3
+ Isolator.isolate :sidekiq,
4
+ target: Sidekiq::Client,
5
+ method_name: :raw_push,
6
+ exception_class: Isolator::BackgroundJobError
@@ -15,17 +15,36 @@ module Isolator
15
15
  end
16
16
 
17
17
  def enabled?
18
- @disabled != true
18
+ !disabled?
19
19
  end
20
20
 
21
- def notify(backtrace)
21
+ def disabled?
22
+ @disabled == true
23
+ end
24
+
25
+ def notify(backtrace, *args)
26
+ return unless notify?(*args)
22
27
  Isolator.notify(exception: build_exception, backtrace: backtrace)
23
28
  end
24
29
 
25
- def notify_isolator?
26
- enabled? && Isolator.within_transaction?
30
+ def notify?(*args)
31
+ enabled? && Isolator.enabled? && Isolator.within_transaction? && !ignored?(*args)
27
32
  end
28
33
 
34
+ def ignore_if
35
+ ignores << Proc.new
36
+ end
37
+
38
+ def ignores
39
+ @ignores ||= []
40
+ end
41
+
42
+ def ignored?(*args)
43
+ ignores.any? { |block| block.call(*args) }
44
+ end
45
+
46
+ private
47
+
29
48
  def build_exception
30
49
  klass = exception_class || Isolator::UnsafeOperationError
31
50
  klass.new(exception_message)
@@ -1,20 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sniffer"
3
+ require "isolator/adapters/http/sniffer"
4
4
 
5
- Sniffer.config do |c|
6
- # Disable Sniffer logger
7
- c.logger = Logger.new(IO::NULL)
8
- end
9
-
10
- Isolator.isolate :http, Sniffer.singleton_class,
11
- :store, exception_class: Isolator::HTTPError
12
-
13
- Isolator.before_isolate do
14
- Sniffer.enable!
15
- end
16
-
17
- Isolator.after_isolate do
18
- Sniffer.clear!
19
- Sniffer.disable!
20
- end
5
+ require "isolator/adapters/http/webmock" if defined?(::WebMock)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sniffer"
4
+
5
+ # Disable Sniffer logger
6
+ Sniffer::Config.defaults["logger"] = Logger.new(IO::NULL)
7
+
8
+ Isolator.isolate :http, target: Sniffer.singleton_class,
9
+ method_name: :store,
10
+ exception_class: Isolator::HTTPError
11
+
12
+ Isolator.before_isolate do
13
+ next if Isolator.adapters.http.disabled?
14
+ Sniffer.enable!
15
+ end
16
+
17
+ Isolator.after_isolate do
18
+ next if Isolator.adapters.http.disabled?
19
+ Sniffer.clear!
20
+ Sniffer.disable!
21
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ adapter = Isolator.isolate :webmock, exception_class: Isolator::HTTPError
4
+
5
+ WebMock.after_request do |*args|
6
+ adapter.notify(caller, *args)
7
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Isolator.isolate :mailer, Mail::Message, :deliver,
4
- exception_class: Isolator::MailerError
3
+ Isolator.isolate :mailer, target: Mail::Message, method_name: :deliver,
4
+ exception_class: Isolator::MailerError
@@ -12,10 +12,12 @@ module Isolator
12
12
  end
13
13
 
14
14
  def start!
15
+ return if Isolator.disabled?
15
16
  before_isolate_callbacks.each(&:call)
16
17
  end
17
18
 
18
19
  def finish!
20
+ return if Isolator.disabled?
19
21
  after_isolate_callbacks.each(&:call)
20
22
  end
21
23
 
@@ -3,9 +3,9 @@
3
3
  module Isolator
4
4
  # Add .isolate function to build and register adapters
5
5
  module Isolate
6
- def isolate(id, target_module, method_name, **options)
6
+ def isolate(id, **options)
7
7
  raise "Adapter already registered: #{id}" if Isolator.adapters.key?(id.to_s)
8
- adapter = AdapterBuilder.call(target_module, method_name, **options)
8
+ adapter = AdapterBuilder.call(**options)
9
9
  Isolator.adapters[id.to_s] = adapter
10
10
  end
11
11
  end
@@ -28,11 +28,15 @@ module Isolator
28
28
 
29
29
  def log_exception
30
30
  return unless Isolator.config.logger
31
- Isolator.config.logger.warn(
32
- "[ISOLATOR EXCEPTION]\n" \
33
- "#{exception.message}\n" \
34
- " #{filtered_backtrace.first}"
35
- )
31
+
32
+ offense_line = filtered_backtrace.first
33
+
34
+ msg = "[ISOLATOR EXCEPTION]\n" \
35
+ "#{exception.message}"
36
+
37
+ msg += "\n ↳ #{offense_line}" if offense_line
38
+
39
+ Isolator.config.logger.warn(msg)
36
40
  end
37
41
 
38
42
  def send_notifications
@@ -44,7 +48,7 @@ module Isolator
44
48
  end
45
49
 
46
50
  def filtered_backtrace
47
- backtrace.reject { |line| line =~ /gems/ }.take_while { |line| line !~ /ruby/ }
51
+ backtrace.reject { |line| line =~ /\/(gems|ruby)/ }.take_while { |line| line !~ /ruby/ }
48
52
  end
49
53
 
50
54
  def uniform_notifier_loaded?
@@ -3,8 +3,9 @@
3
3
  module Isolator
4
4
  class Railtie < ::Rails::Railtie # :nodoc:
5
5
  config.after_initialize do
6
- # Load adapters
7
- require "isolator/adapters"
6
+ # Forec load adapters after application initialization
7
+ # (when all deps are likely to be loaded).
8
+ load File.join(__dir__, "adapters.rb")
8
9
 
9
10
  next unless Rails.env.test?
10
11
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Isolator
4
- VERSION = "0.1.0.pre2"
4
+ VERSION = "0.1.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: isolator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre2
4
+ version: 0.1.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-02-18 00:00:00.000000000 Z
11
+ date: 2018-02-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sniffer
@@ -150,6 +150,34 @@ dependencies:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
152
  version: '5.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: webmock
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '3.1'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '3.1'
167
+ - !ruby/object:Gem::Dependency
168
+ name: test_after_commit
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '1.1'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '1.1'
153
181
  description: Detect non-atomic interactions within DB transactions
154
182
  email:
155
183
  - dementiev.vm@gmail.com
@@ -174,11 +202,14 @@ files:
174
202
  - lib/isolator.rb
175
203
  - lib/isolator/adapter_builder.rb
176
204
  - lib/isolator/adapters.rb
205
+ - lib/isolator/adapters/after_commit.rb
177
206
  - lib/isolator/adapters/background_jobs.rb
178
207
  - lib/isolator/adapters/background_jobs/active_job.rb
179
208
  - lib/isolator/adapters/background_jobs/sidekiq.rb
180
209
  - lib/isolator/adapters/base.rb
181
210
  - lib/isolator/adapters/http.rb
211
+ - lib/isolator/adapters/http/sniffer.rb
212
+ - lib/isolator/adapters/http/webmock.rb
182
213
  - lib/isolator/adapters/mailers.rb
183
214
  - lib/isolator/adapters/mailers/mail.rb
184
215
  - lib/isolator/callbacks.rb
@@ -209,9 +240,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
209
240
  version: 2.3.0
210
241
  required_rubygems_version: !ruby/object:Gem::Requirement
211
242
  requirements:
212
- - - ">"
243
+ - - ">="
213
244
  - !ruby/object:Gem::Version
214
- version: 1.3.1
245
+ version: '0'
215
246
  requirements: []
216
247
  rubyforge_project:
217
248
  rubygems_version: 2.7.4