isolator 0.1.0.pre2 → 0.1.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: 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