isolator 0.7.0 → 0.9.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: 00d85d8a62a5a34bd39ef9295e1e8ad9c13599fc17648bbda7a5012f9a89ddfa
4
- data.tar.gz: cf7a441154f63bc40258b4a78e57e3d3f46158b915167c1631d717530571dcd2
3
+ metadata.gz: 529433e0e838e663f335a5fb493f51d10f16fbde2168c89be72e92c968709593
4
+ data.tar.gz: ef33db058e25204e615fae7ee945c738e516e8ae6d63bb93f9660e6e117cb632
5
5
  SHA512:
6
- metadata.gz: a088b0f2e24c8afd8a2ec3fd65df4ef5e2d550f55c8376132955db8f870f68a28f021b8079024b413ef2692a474136f3d7da56c098a89d2e3b3aa52deb0ce334
7
- data.tar.gz: d3667e26d54697e09eaefd7cdee7e9c5586e307c371ce41215d8eca88dc514c652079088bba2af3aeb654b9bfbdad3a3ab6080c5f20b47867291cf4734f16cda
6
+ metadata.gz: 5cb63aa9a96f5931425267193590550cac0614dd15d272bd7d33f7e2aeaad0b7b37a19f5a6f7d8617dce992e946b92c42c3df5829177cbd51b41d8919abdc8c5
7
+ data.tar.gz: b4997855a9d813a961491235097a42eee75d55598fb7d33d53bb4f7e3828f486ea0a2fb788edd028237e39518a2d8e962824b08a3f4cbed2ce3ac22acdfee9c3
data/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.9.0 (2023-05-18)
6
+
7
+ - Support keyword arguments to isolated method in Ruby 3.0. ([@Mange][])
8
+ - Raise an error when an ignore file does not parse to a hash. ([@bobbymcwho][])
9
+ - Log all filtered backtrace lines to the logger ([@bobbymcwho][])
10
+ - Add support for removing dynamic adapters. ([@Mange][])
11
+ - Allow aliases in .isolator_todo.yml and .isolator_ignore.yml ([@tomgi][])
12
+
13
+ ## 0.8.0 (2021-12-29)
14
+
15
+ - Drop Ruby 2.5 support.
16
+
17
+ - Add .isolator_ignore.yml configuration file for Rails application.
18
+
5
19
  ## 0.7.0 (2020-09-25)
6
20
 
7
21
  - Add debug mode. ([@palkan][])
@@ -93,3 +107,5 @@ This, for example, makes Isolator compatible with Rails multi-database apps.
93
107
  [@shivanshgaur]: https://github.com/shivanshgaur
94
108
  [@iiwo]: https://github.com/iiwo
95
109
  [@mquan]: https://github.com/mquan
110
+ [@bobbymcwho]: https://github.com/bobbymcwho
111
+ [@Mange]: https://github.com/Mange
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018-2020 Vladimir Dementyev
3
+ Copyright (c) 2018-2023 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
@@ -104,24 +104,24 @@ Isolator.configure do |config|
104
104
  end
105
105
  ```
106
106
 
107
- Isolator relys on [uniform_notifier][] to send custom notifications.
107
+ Isolator relies on [uniform_notifier][] to send custom notifications.
108
108
 
109
109
  **NOTE:** `uniform_notifier` should be installed separately (i.e., added to Gemfile).
110
110
 
111
111
  ### Transactional tests support
112
112
 
113
- - Rails' baked-in [use_transactional_tests](api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html#class-ActiveRecord::FixtureSet-label-Transactional+Tests)
113
+ - Rails' baked-in [use_transactional_tests](https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html#class-ActiveRecord::FixtureSet-label-Transactional+Tests)
114
114
  - [database_cleaner](https://github.com/DatabaseCleaner/database_cleaner) gem. Make sure that you require isolator _after_ database_cleaner.
115
115
 
116
116
  ### Supported ORMs
117
117
 
118
- - `ActiveRecord` >= 4.1
118
+ - `ActiveRecord` >= 5.1 (4.2 likely till works, but we do not test against it anymore)
119
119
  - `ROM::SQL` (only if Active Support instrumentation extension is loaded)
120
120
 
121
121
  ### Adapters
122
122
 
123
123
  Isolator has a bunch of built-in adapters:
124
- - `:http` – built on top of [Sniffer][]
124
+ - `:http` – built on top of [Sniffer][]
125
125
  - `:active_job`
126
126
  - `:sidekiq`
127
127
  - `:resque`
@@ -159,9 +159,32 @@ Isolator.adapters.sidekiq.ignore_if { Thread.current[:sidekiq_postpone] }
159
159
 
160
160
  You can add as many _ignores_ as you want, the offense is registered iff all of them return false.
161
161
 
162
+
163
+ ### Using with sidekiq/testing
164
+
165
+ If you require sidekiq/testing in your tests after isolator is required then it will blow away isolator's hooks, so you need to require isolator after requiring sidekiq/testing.
166
+
167
+ If you're using Rails and want to use isolator in development and staging, then here is a way to do this.
168
+
169
+ ```ruby
170
+
171
+ # Gemfile
172
+ gem "isolator", require: false # so it delays loading till after sidekiq/testing
173
+
174
+ # config/initializers/isolator.rb
175
+ require "sidekiq/testing" if Rails.env.test?
176
+
177
+ unless Rails.env.production? # so we get it in staging too
178
+ require "isolator"
179
+ Isolator.configure do |config|
180
+ config.send_notifications = true # ...
181
+ end
182
+ end
183
+ ```
184
+
162
185
  ### Using with legacy Rails codebases
163
186
 
164
- If you already have a huge Rails project it can be a tricky to turn Isolator on because you'll immediately get a lot of failed specs. If you want to fix detected issues one by one, you can list all of them in the special file `.isolator_todo.yml` in a following way:
187
+ If you already have a huge Rails project it can be tricky to turn Isolator on because you'll immediately get a lot of failed specs. If you want to fix detected issues one by one, you can list all of them in the special files `.isolator_todo.yml` and `.isolator_ignore.yml` in the following way:
165
188
 
166
189
  ```
167
190
  sidekiq:
@@ -169,8 +192,20 @@ sidekiq:
169
192
  - app/models/sales/**/*.rb
170
193
  ```
171
194
 
195
+ You can ignore the same files in multiple adapters using YML aliases in the following way:
196
+
197
+ ```
198
+ http_common: &http_common
199
+ - app/models/user.rb:20
200
+
201
+ http: *http_common
202
+ webmock: *http_common
203
+ ```
204
+
172
205
  All the exceptions raised in the listed lines will be ignored.
173
206
 
207
+ The `.isolator_todo.yml` file is intended to point to the code that should be fixed later, and `.isolator_ignore.yml` points to the code that for some reasons is not expected to be fixed. (See https://github.com/palkan/isolator/issues/40)
208
+
174
209
  ### Using with legacy Ruby codebases
175
210
 
176
211
  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")`
@@ -196,16 +231,43 @@ Isolator.isolate :danger, Danger.singleton_class, :explode, options
196
231
  Possible `options` are:
197
232
  - `exception_class` – an exception class to raise in case of offense
198
233
  - `exception_message` – custom exception message (could be specified without a class)
199
- - `details_message` – a block to generate additional exceptin message information:
234
+ - `details_message` – a block to generate additional exception message information:
200
235
 
201
236
  ```ruby
202
237
  Isolator.isolate :active_job,
203
238
  target: ActiveJob::Base,
204
239
  method_name: :enqueue,
205
240
  exception_class: Isolator::BackgroundJobError,
206
- details_message: ->(obj, _args) {
241
+ details_message: ->(obj) {
207
242
  "#{obj.class.name}(#{obj.arguments})"
208
243
  }
244
+
245
+ Isolator.isolate :promoter,
246
+ target: UserPromoter,
247
+ method_name: :call,
248
+ details_message: ->(obj_, args, kwargs) {
249
+ # UserPromoter.call(user, role, by: nil)
250
+ user, role = args
251
+ by = kwargs[:by]
252
+ "#{user.name} promoted to #{role} by #{by&.name || "system"})"
253
+ }
254
+ ```
255
+
256
+ Trying to register the same adapter name twice will raise an error. You can guard for it, or remove old adapters before in order to replace them.
257
+
258
+ ```ruby
259
+ unless Isolator.has_adapter?(:promoter)
260
+ Isolator.isolate(:promoter, *rest)
261
+ end
262
+ ```
263
+
264
+ ```ruby
265
+ # Handle code reloading
266
+ class Messager
267
+ end
268
+
269
+ Isolator.remove_adapter(:messager)
270
+ Isolator.isolate(:messager, target: Messager, **rest)
209
271
  ```
210
272
 
211
273
  You can also add some callbacks to be run before and after the transaction:
@@ -5,29 +5,39 @@ require "isolator/adapters/base"
5
5
  module Isolator
6
6
  # Builds adapter from provided params
7
7
  module AdapterBuilder
8
- def self.call(target: nil, method_name: nil, **options)
9
- adapter = Module.new do
10
- extend Isolator::Adapters::Base
8
+ class << self
9
+ def call(target: nil, method_name: nil, **options)
10
+ adapter = Module.new do
11
+ extend Isolator::Adapters::Base
11
12
 
12
- self.exception_class = options[:exception_class] if options.key?(:exception_class)
13
- self.exception_message = options[:exception_message] if options.key?(:exception_message)
14
- self.details_message = options[:details_message] if options.key?(:details_message)
13
+ self.exception_class = options[:exception_class] if options.key?(:exception_class)
14
+ self.exception_message = options[:exception_message] if options.key?(:exception_message)
15
+ self.details_message = options[:details_message] if options.key?(:details_message)
16
+ end
17
+
18
+ mod = build_mod(method_name, adapter)
19
+ if target && mod
20
+ target.prepend(mod)
21
+ adapter.define_singleton_method(:restore) do
22
+ mod.remove_method(method_name)
23
+ end
24
+ end
25
+
26
+ adapter
15
27
  end
16
28
 
17
- add_patch_method(adapter, target, method_name) if
18
- target && method_name
19
- adapter
20
- end
29
+ private
30
+
31
+ def build_mod(method_name, adapter)
32
+ return nil unless method_name
21
33
 
22
- def self.add_patch_method(adapter, base, method_name)
23
- mod = Module.new do
24
- define_method method_name do |*args, &block|
25
- adapter.notify(caller, self, *args)
26
- super(*args, &block)
34
+ Module.new do
35
+ define_method method_name do |*args, **kwargs, &block|
36
+ adapter.notify(caller, self, *args, **kwargs)
37
+ super(*args, **kwargs, &block)
38
+ end
27
39
  end
28
40
  end
29
-
30
- base.prepend mod
31
41
  end
32
42
  end
33
43
  end
@@ -4,7 +4,7 @@ Isolator.isolate :active_job,
4
4
  target: ActiveJob::Base,
5
5
  method_name: :enqueue,
6
6
  exception_class: Isolator::BackgroundJobError,
7
- details_message: ->(obj, _args) {
7
+ details_message: ->(obj) {
8
8
  "#{obj.class.name}" \
9
9
  "#{obj.arguments.any? ? " (#{obj.arguments.join(", ")})" : ""}"
10
10
  }
@@ -22,13 +22,13 @@ module Isolator
22
22
  @disabled == true
23
23
  end
24
24
 
25
- def notify(backtrace, obj, *args)
26
- return unless notify?(*args)
27
- Isolator.notify(exception: build_exception(obj, args), backtrace: backtrace)
25
+ def notify(backtrace, obj, *args, **kwargs)
26
+ return unless notify?(*args, **kwargs)
27
+ Isolator.notify(exception: build_exception(obj, args, kwargs), backtrace: backtrace)
28
28
  end
29
29
 
30
- def notify?(*args)
31
- enabled? && Isolator.enabled? && Isolator.within_transaction? && !ignored?(*args)
30
+ def notify?(*args, **kwargs)
31
+ enabled? && Isolator.enabled? && Isolator.within_transaction? && !ignored?(*args, **kwargs)
32
32
  end
33
33
 
34
34
  def ignore_if(&block)
@@ -39,17 +39,36 @@ module Isolator
39
39
  @ignores ||= []
40
40
  end
41
41
 
42
- def ignored?(*args)
43
- ignores.any? { |block| block.call(*args) }
42
+ def ignored?(*args, **kwargs)
43
+ ignores.any? { |block| block.call(*args, **kwargs) }
44
44
  end
45
45
 
46
46
  private
47
47
 
48
- def build_exception(obj, args)
48
+ def build_exception(obj, args, kwargs = {})
49
49
  klass = exception_class || Isolator::UnsafeOperationError
50
- details = details_message.call(obj, args) if details_message
50
+ details = build_details(obj, args, kwargs)
51
51
  klass.new(exception_message, details: details)
52
52
  end
53
+
54
+ def build_details(obj, args, kwargs)
55
+ return nil unless details_message
56
+
57
+ case details_message.arity
58
+ when 2, -2
59
+ # Older users of details_message expected only two arguments. Add
60
+ # kwargs hash as last argument, like in older Ruby.
61
+ details_message.call(obj, args + [kwargs])
62
+ when 3, -3
63
+ # New signature separates args from kwargs
64
+ details_message.call(obj, args, kwargs)
65
+ when 1
66
+ # Callback does not care about any args
67
+ details_message.call(obj)
68
+ else
69
+ raise "Unexpected arity (#{details_message.arity}) for #{details_message.inspect}"
70
+ end
71
+ end
53
72
  end
54
73
  end
55
74
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  adapter = Isolator.isolate :webmock,
4
4
  exception_class: Isolator::HTTPError,
5
- details_message: ->(obj, _args) {
5
+ details_message: ->(obj) {
6
6
  "#{obj.method.to_s.upcase} #{obj.uri}"
7
7
  }
8
8
 
@@ -3,7 +3,7 @@
3
3
  Isolator.isolate :mailer, target: Mail::Message,
4
4
  method_name: :deliver,
5
5
  exception_class: Isolator::MailerError,
6
- details_message: ->(obj, _args) {
6
+ details_message: ->(obj) {
7
7
  "From: #{obj.from}\n" \
8
8
  "To: #{obj.to}\n" \
9
9
  "Subject: #{obj.subject}"
@@ -28,8 +28,8 @@ module Isolator
28
28
  @ignorer = Isolator::Ignorer
29
29
  end
30
30
 
31
- alias raise_exceptions? raise_exceptions
32
- alias send_notifications? send_notifications
31
+ alias_method :raise_exceptions?, :raise_exceptions
32
+ alias_method :send_notifications?, :send_notifications
33
33
 
34
34
  def test_env?
35
35
  ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test"
@@ -3,16 +3,31 @@
3
3
  module Isolator
4
4
  # Handle ignoring isolator errors using a yml file
5
5
  class Ignorer
6
- TODO_PATH = ".isolator_todo.yml"
6
+ class ParseError < StandardError
7
+ def initialize(file_path, klass)
8
+ @file_path = file_path
9
+ @klass = klass
10
+ end
11
+
12
+ def message
13
+ "Unable to parse ignore config file #{@file_path}. Expected Hash, got #{@klass}."
14
+ end
15
+ end
7
16
 
8
17
  class << self
9
- def prepare(path: TODO_PATH, regex_string: "^.*(#ignores#):.*$")
18
+ def prepare(path:, regex_string: "^.*(#ignores#):.*$")
10
19
  return unless File.exist?(path)
11
20
 
12
- todos = YAML.load_file(path)
21
+ ignores = begin
22
+ YAML.load_file(path, aliases: true)
23
+ rescue ArgumentError # support for older rubies https://github.com/rails/rails/commit/179d0a1f474ada02e0030ac3bd062fc653765dbe
24
+ YAML.load_file(path)
25
+ end
26
+
27
+ raise ParseError.new(path, ignores.class) unless ignores.respond_to?(:fetch)
13
28
 
14
29
  Isolator.adapters.each do |id, adapter|
15
- ignored_paths = todos.fetch(id, [])
30
+ ignored_paths = ignores.fetch(id, [])
16
31
  AdapterIgnore.new(adapter: adapter, ignored_paths: ignored_paths, regex_string: regex_string).prepare
17
32
  end
18
33
  end
@@ -4,9 +4,15 @@ module Isolator
4
4
  # Add .isolate function to build and register adapters
5
5
  module Isolate
6
6
  def isolate(id, **options)
7
- raise "Adapter already registered: #{id}" if Isolator.adapters.key?(id.to_s)
7
+ raise "Adapter already registered: #{id}" if Isolator.has_adapter?(id)
8
8
  adapter = AdapterBuilder.call(**options)
9
9
  Isolator.adapters[id.to_s] = adapter
10
10
  end
11
+
12
+ def remove_adapter(id)
13
+ if (adapter = Isolator.adapters.delete(id.to_s))
14
+ adapter.restore if adapter.respond_to?(:restore)
15
+ end
16
+ end
11
17
  end
12
18
  end
@@ -29,12 +29,12 @@ module Isolator
29
29
  def log_exception
30
30
  return unless Isolator.config.logger
31
31
 
32
- offense_line = filtered_backtrace.first
33
-
34
32
  msg = "[ISOLATOR EXCEPTION]\n" \
35
33
  "#{exception.message}"
36
34
 
37
- msg += "\n ↳ #{offense_line}" if offense_line
35
+ filtered_backtrace.each do |offense_line|
36
+ msg += "\n ↳ #{offense_line}"
37
+ end
38
38
 
39
39
  Isolator.config.logger.warn(msg)
40
40
  end
@@ -15,7 +15,19 @@ module Isolator
15
15
  # (when all deps are likely to be loaded).
16
16
  load File.join(__dir__, "adapters.rb")
17
17
 
18
- Isolator.config.ignorer&.prepare
18
+ # Try to load Rails base classes to trigger their load hooks
19
+ begin
20
+ ::ActionMailer::Base
21
+ rescue NameError
22
+ end
23
+
24
+ begin
25
+ ::ActiveJob::Base
26
+ rescue NameError
27
+ end
28
+
29
+ Isolator.config.ignorer&.prepare(path: ".isolator_todo.yml")
30
+ Isolator.config.ignorer&.prepare(path: ".isolator_ignore.yml")
19
31
 
20
32
  next unless Rails.env.test?
21
33
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Isolator
4
- VERSION = "0.7.0"
4
+ VERSION = "0.9.0"
5
5
  end
data/lib/isolator.rb CHANGED
@@ -135,6 +135,13 @@ module Isolator
135
135
  end
136
136
 
137
137
  def decr_transactions!(connection_id = default_connection_id.call)
138
+ current = state[:transactions]&.[](connection_id) || 0
139
+
140
+ if current <= 0
141
+ warn "Trying to finalize an untracked transaction"
142
+ return
143
+ end
144
+
138
145
  state[:transactions][connection_id] -= 1
139
146
 
140
147
  finish! if current_transactions(connection_id) == (connection_threshold(connection_id) - 1)
@@ -167,6 +174,10 @@ module Isolator
167
174
  @adapters ||= Isolator::SimpleHashie.new
168
175
  end
169
176
 
177
+ def has_adapter?(id)
178
+ adapters.key?(id.to_s)
179
+ end
180
+
170
181
  def load_ignore_config(path)
171
182
  warn "[DEPRECATION] `load_ignore_config` is deprecated. Please use `Isolator::Ignorer.prepare` instead."
172
183
  Isolator::Ignorer.prepare(path: path)
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.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-25 00:00:00.000000000 Z
11
+ date: 2023-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sniffer
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.3.1
19
+ version: 0.5.0
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
- version: 0.3.1
26
+ version: 0.5.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -248,6 +248,34 @@ dependencies:
248
248
  - - ">="
249
249
  - !ruby/object:Gem::Version
250
250
  version: '0'
251
+ - !ruby/object:Gem::Dependency
252
+ name: webrick
253
+ requirement: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - ">="
256
+ - !ruby/object:Gem::Version
257
+ version: '0'
258
+ type: :development
259
+ prerelease: false
260
+ version_requirements: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - ">="
263
+ - !ruby/object:Gem::Version
264
+ version: '0'
265
+ - !ruby/object:Gem::Dependency
266
+ name: net-smtp
267
+ requirement: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - ">="
270
+ - !ruby/object:Gem::Version
271
+ version: '0'
272
+ type: :development
273
+ prerelease: false
274
+ version_requirements: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - ">="
277
+ - !ruby/object:Gem::Version
278
+ version: '0'
251
279
  description: Detect non-atomic interactions within DB transactions
252
280
  email:
253
281
  - dementiev.vm@gmail.com
@@ -298,7 +326,8 @@ metadata:
298
326
  documentation_uri: http://github.com/palkan/isolator
299
327
  homepage_uri: http://github.com/palkan/isolator
300
328
  source_code_uri: http://github.com/palkan/isolator
301
- post_install_message:
329
+ funding_uri: https://github.com/sponsors/palkan
330
+ post_install_message:
302
331
  rdoc_options: []
303
332
  require_paths:
304
333
  - lib
@@ -306,15 +335,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
306
335
  requirements:
307
336
  - - ">="
308
337
  - !ruby/object:Gem::Version
309
- version: 2.5.0
338
+ version: 2.6.0
310
339
  required_rubygems_version: !ruby/object:Gem::Requirement
311
340
  requirements:
312
341
  - - ">="
313
342
  - !ruby/object:Gem::Version
314
343
  version: '0'
315
344
  requirements: []
316
- rubygems_version: 3.0.6
317
- signing_key:
345
+ rubygems_version: 3.4.8
346
+ signing_key:
318
347
  specification_version: 4
319
348
  summary: Detect non-atomic interactions within DB transactions
320
349
  test_files: []