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 +4 -4
- data/CHANGELOG.md +10 -0
- data/LICENSE.txt +1 -1
- data/README.md +62 -2
- data/lib/isolator/adapter_builder.rb +27 -17
- data/lib/isolator/adapters/background_jobs/active_job.rb +1 -1
- data/lib/isolator/adapters/base.rb +28 -9
- data/lib/isolator/adapters/http/webmock.rb +1 -1
- data/lib/isolator/adapters/mailers/mail.rb +1 -1
- data/lib/isolator/ignorer.rb +19 -4
- data/lib/isolator/isolate.rb +7 -1
- data/lib/isolator/notifier.rb +3 -3
- data/lib/isolator/railtie.rb +12 -1
- data/lib/isolator/version.rb +1 -1
- data/lib/isolator.rb +4 -0
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 529433e0e838e663f335a5fb493f51d10f16fbde2168c89be72e92c968709593
|
4
|
+
data.tar.gz: ef33db058e25204e615fae7ee945c738e516e8ae6d63bb93f9660e6e117cb632
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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-
|
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
|
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
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
18
|
-
|
19
|
-
adapter
|
20
|
-
|
29
|
+
private
|
30
|
+
|
31
|
+
def build_mod(method_name, adapter)
|
32
|
+
return nil unless method_name
|
21
33
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
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 =
|
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
|
data/lib/isolator/ignorer.rb
CHANGED
@@ -3,16 +3,31 @@
|
|
3
3
|
module Isolator
|
4
4
|
# Handle ignoring isolator errors using a yml file
|
5
5
|
class Ignorer
|
6
|
-
|
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
|
18
|
+
def prepare(path:, regex_string: "^.*(#ignores#):.*$")
|
10
19
|
return unless File.exist?(path)
|
11
20
|
|
12
|
-
|
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 =
|
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
|
data/lib/isolator/isolate.rb
CHANGED
@@ -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.
|
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
|
data/lib/isolator/notifier.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/isolator/railtie.rb
CHANGED
@@ -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
|
-
|
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?
|
data/lib/isolator/version.rb
CHANGED
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.
|
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:
|
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.
|
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.
|
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.
|
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
|