isolator 0.8.0 → 0.9.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: 5c40700b0b2f7e5025ed7cbfb80580ac9ee1516a1295146bf0fa9db1de6e2a7f
4
- data.tar.gz: 7d605d3790733c0b1bcd44e6188c30050900879042ef604950cc38b36d6214d0
3
+ metadata.gz: 529433e0e838e663f335a5fb493f51d10f16fbde2168c89be72e92c968709593
4
+ data.tar.gz: ef33db058e25204e615fae7ee945c738e516e8ae6d63bb93f9660e6e117cb632
5
5
  SHA512:
6
- metadata.gz: d3a05d4ccad1f80171a67f0f124cb2d37548803a5451338cb9d821c16042dc94c407f9a4a155597742268cbf8ee939594a166c49835f35f5f4d2ec4d1d9a0567
7
- data.tar.gz: a287d291ca6465f3c8acfc8cb8c1c5ba898315c6aae80156855153d13a3c7c763864d97127ef38b04331bb7c99b23b417783eada86afdf8c26282c694dfe22bd
6
+ metadata.gz: 5cb63aa9a96f5931425267193590550cac0614dd15d272bd7d33f7e2aeaad0b7b37a19f5a6f7d8617dce992e946b92c42c3df5829177cbd51b41d8919abdc8c5
7
+ data.tar.gz: b4997855a9d813a961491235097a42eee75d55598fb7d33d53bb4f7e3828f486ea0a2fb788edd028237e39518a2d8e962824b08a3f4cbed2ce3ac22acdfee9c3
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
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
+
5
13
  ## 0.8.0 (2021-12-29)
6
14
 
7
15
  - Drop Ruby 2.5 support.
@@ -99,3 +107,5 @@ This, for example, makes Isolator compatible with Rails multi-database apps.
99
107
  [@shivanshgaur]: https://github.com/shivanshgaur
100
108
  [@iiwo]: https://github.com/iiwo
101
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
@@ -110,7 +110,7 @@ Isolator relies on [uniform_notifier][] to send custom notifications.
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
@@ -159,6 +159,29 @@ 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
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:
@@ -169,6 +192,16 @@ 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
 
174
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)
@@ -205,9 +238,36 @@ Isolator.isolate :active_job,
205
238
  target: ActiveJob::Base,
206
239
  method_name: :enqueue,
207
240
  exception_class: Isolator::BackgroundJobError,
208
- details_message: ->(obj, _args) {
241
+ details_message: ->(obj) {
209
242
  "#{obj.class.name}(#{obj.arguments})"
210
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)
211
271
  ```
212
272
 
213
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}"
@@ -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,18 @@ 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")
19
30
  Isolator.config.ignorer&.prepare(path: ".isolator_ignore.yml")
20
31
 
21
32
  next unless Rails.env.test?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Isolator
4
- VERSION = "0.8.0"
4
+ VERSION = "0.9.0"
5
5
  end
data/lib/isolator.rb CHANGED
@@ -174,6 +174,10 @@ module Isolator
174
174
  @adapters ||= Isolator::SimpleHashie.new
175
175
  end
176
176
 
177
+ def has_adapter?(id)
178
+ adapters.key?(id.to_s)
179
+ end
180
+
177
181
  def load_ignore_config(path)
178
182
  warn "[DEPRECATION] `load_ignore_config` is deprecated. Please use `Isolator::Ignorer.prepare` instead."
179
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.8.0
4
+ version: 0.9.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: 2021-12-29 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
@@ -326,6 +326,7 @@ metadata:
326
326
  documentation_uri: http://github.com/palkan/isolator
327
327
  homepage_uri: http://github.com/palkan/isolator
328
328
  source_code_uri: http://github.com/palkan/isolator
329
+ funding_uri: https://github.com/sponsors/palkan
329
330
  post_install_message:
330
331
  rdoc_options: []
331
332
  require_paths:
@@ -341,7 +342,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
341
342
  - !ruby/object:Gem::Version
342
343
  version: '0'
343
344
  requirements: []
344
- rubygems_version: 3.2.22
345
+ rubygems_version: 3.4.8
345
346
  signing_key:
346
347
  specification_version: 4
347
348
  summary: Detect non-atomic interactions within DB transactions