isolator 0.7.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: 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: []