factory_sloth 1.1.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +10 -3
- data/lib/factory_sloth/code_mod.rb +44 -16
- data/lib/factory_sloth/create_call.rb +1 -0
- data/lib/factory_sloth/create_call_finder.rb +27 -11
- data/lib/factory_sloth/file_processor.rb +1 -1
- data/lib/factory_sloth/spec_runner.rb +9 -9
- data/lib/factory_sloth/version.rb +1 -1
- data/lib/factory_sloth.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ce07258dd3b7b7e17ba54accb86be394e97f3f7c986b7d05705dcd54b93e6d50
|
4
|
+
data.tar.gz: 3160fc1436b8f9271f94b005a367c18084298c003623ca97bb773c9b10656f32
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 44dec49c5e35641d9a92bf175bed7afa4a9a58a87e4c663fb50545aaec7551ee49ca88e61d827053c0505d0d0ae9e59d323a7eba3d059f7361d9cc610ba9cf08
|
7
|
+
data.tar.gz: 0ada9a7c4f0a787bef23d67d4bdcb1dbf80b22294b13467ccdf1b0ed0f044017cac9ffe2e2db4b61622f0da19996b3d4f6d733947fd1f1b3b45e6283ae773abf
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.2.1] - 2023-05-17
|
4
|
+
|
5
|
+
### Fixed
|
6
|
+
|
7
|
+
- Fixed handling of path-based, derived spec metadata
|
8
|
+
- Thanks to https://github.com/bquorning
|
9
|
+
- Fixed unnecessary spec runs for files without create calls or changes
|
10
|
+
- Fixed modification of factory calls that never run (e.g. in skipped examples)
|
11
|
+
|
12
|
+
## [1.2.0] - 2023-05-16
|
13
|
+
|
14
|
+
### Added
|
15
|
+
|
16
|
+
- Added support for magic comments `# sloth:disable`, `# sloth:enable`
|
17
|
+
|
3
18
|
## [1.1.0] - 2023-05-16
|
4
19
|
|
5
20
|
### Added
|
data/README.md
CHANGED
@@ -56,10 +56,17 @@ Scanned 4 files, found 2 unnecessary create calls across 1 files and 1 broken sp
|
|
56
56
|
|
57
57
|
The `conflict` case is rare. It only happens if individual examples were green after changing them, but at least one example failed when evaluating the whole file after all changes. This probably means that some other example was red even before making changes, or that something else is wrong with this spec file, e.g. some examples depend on other examples' side effects.
|
58
58
|
|
59
|
-
## Limitations
|
59
|
+
## Limitations / known issues
|
60
60
|
|
61
|
-
|
62
|
-
|
61
|
+
FactorySloth only works with RSpec so far. It also generates false positives in cases where create calls are done but only the *absence* of any effect is tested, e.g.:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
user1 = create(:user, in_search: true)
|
65
|
+
user2 = create(:user, in_search: false)
|
66
|
+
expect(User.searchable).to eq(user1)
|
67
|
+
```
|
68
|
+
|
69
|
+
This test will still pass if `user2` is no longer inserted into the database, leading factory_sloth to believe that `build` suffices here. However, this makes the test no longer assert the same thing. `# sloth:disable` / `# sloth:enable` comments can be used to prevent `factory_sloth` from making such changes. If you have a good idea about how to detect such cases automatically, let me know :)
|
63
70
|
|
64
71
|
## Development
|
65
72
|
|
@@ -1,11 +1,12 @@
|
|
1
1
|
class FactorySloth::CodeMod
|
2
|
-
attr_reader :create_calls, :changed_create_calls, :original_code, :patched_code
|
2
|
+
attr_reader :create_calls, :changed_create_calls, :path, :original_code, :patched_code
|
3
3
|
|
4
|
-
def self.call(code)
|
5
|
-
new(code).tap(&:call)
|
4
|
+
def self.call(path, code)
|
5
|
+
new(path, code).tap(&:call)
|
6
6
|
end
|
7
7
|
|
8
|
-
def initialize(code)
|
8
|
+
def initialize(path, code)
|
9
|
+
self.path = path
|
9
10
|
self.original_code = code
|
10
11
|
self.patched_code = code
|
11
12
|
end
|
@@ -18,14 +19,15 @@ class FactorySloth::CodeMod
|
|
18
19
|
# rspec call. However, this would make it impossible to use `--fail-fast`,
|
19
20
|
# and might make examples fail that are not as idempotent as they should be.
|
20
21
|
self.changed_create_calls =
|
21
|
-
create_calls
|
22
|
-
|
23
|
-
|
22
|
+
create_calls
|
23
|
+
.sort_by { |call| [-call.line, -call.column] }
|
24
|
+
.select { |call| try_patch(call, 'build') || try_patch(call, 'build_stubbed') }
|
24
25
|
|
25
26
|
# validate whole spec after changes, e.g. to detect side-effects
|
26
|
-
self.ok =
|
27
|
-
|
28
|
-
|
27
|
+
self.ok = changed_create_calls.none? ||
|
28
|
+
FactorySloth::SpecRunner.call(path, patched_code)
|
29
|
+
changed_create_calls.clear unless ok?
|
30
|
+
patched_code.replace(original_code) unless ok?
|
29
31
|
end
|
30
32
|
|
31
33
|
def ok?
|
@@ -46,14 +48,40 @@ class FactorySloth::CodeMod
|
|
46
48
|
|
47
49
|
private
|
48
50
|
|
49
|
-
attr_writer :create_calls, :changed_create_calls, :ok, :original_code, :patched_code
|
51
|
+
attr_writer :create_calls, :changed_create_calls, :ok, :path, :original_code, :patched_code
|
50
52
|
|
51
|
-
def try_patch(
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
def try_patch(call, base_variant)
|
54
|
+
variant = call.name.sub('create', base_variant)
|
55
|
+
new_patched_code = patched_code.sub(
|
56
|
+
/\A(?:.*\n){#{call.line - 1}}.{#{call.column}}\K#{call.name}/,
|
57
|
+
variant
|
58
|
+
)
|
59
|
+
checked_patched_code = with_execution_check(new_patched_code, call.line, variant)
|
60
|
+
if FactorySloth::SpecRunner.call(path, checked_patched_code, line: call.line)
|
61
|
+
puts "- #{call.name} in line #{call.line} can be replaced with #{variant}"
|
56
62
|
self.patched_code = new_patched_code
|
57
63
|
end
|
58
64
|
end
|
65
|
+
|
66
|
+
def with_execution_check(spec_code, line, variant)
|
67
|
+
spec_code + <<~RUBY
|
68
|
+
; defined?(FactoryBot) && defined?(RSpec) && RSpec.configure do |config|
|
69
|
+
executed_lines = []
|
70
|
+
|
71
|
+
FactoryBot::Syntax::Methods.class_eval do
|
72
|
+
alias ___original_#{variant} #{variant}
|
73
|
+
|
74
|
+
define_method("#{variant}") do |*args, **kwargs, &blk|
|
75
|
+
executed_lines << caller_locations(1, 1)&.first&.lineno
|
76
|
+
___original_#{variant}(*args, **kwargs, &blk)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
config.after(:suite) do
|
81
|
+
executed_lines.include?(#{line}) ||
|
82
|
+
fail("unused factory in line #{line} - will not be modified")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
RUBY
|
86
|
+
end
|
59
87
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
FactorySloth::CreateCall = Struct.new(:name, :line, :column, keyword_init: true)
|
@@ -1,35 +1,51 @@
|
|
1
1
|
require 'ripper'
|
2
2
|
|
3
3
|
class FactorySloth::CreateCallFinder < Ripper
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :calls
|
5
5
|
|
6
6
|
def self.call(code:)
|
7
|
-
new(code).tap(&:parse).
|
7
|
+
new(code).tap(&:parse).calls
|
8
8
|
end
|
9
9
|
|
10
|
-
def initialize(...)
|
10
|
+
def initialize(code, ...)
|
11
11
|
super
|
12
|
-
@
|
12
|
+
@code = code
|
13
|
+
@disabled = false
|
14
|
+
@calls = []
|
13
15
|
end
|
14
16
|
private_class_method :new
|
15
17
|
|
18
|
+
def store_call(obj)
|
19
|
+
@calls << obj if obj.is_a?(FactorySloth::CreateCall) && !@disabled
|
20
|
+
end
|
21
|
+
|
16
22
|
def on_ident(name, *)
|
17
|
-
|
23
|
+
%w[create create_list create_pair].include?(name) &&
|
24
|
+
FactorySloth::CreateCall.new(name: name, line: lineno, column: column)
|
18
25
|
end
|
19
26
|
|
20
|
-
def on_call(mod, _,
|
21
|
-
|
27
|
+
def on_call(mod, _, obj, *)
|
28
|
+
store_call(obj) if mod == 'FactoryBot'
|
22
29
|
end
|
23
30
|
|
24
|
-
def on_command_call(mod, _,
|
25
|
-
|
31
|
+
def on_command_call(mod, _, obj, *)
|
32
|
+
store_call(obj) if mod == 'FactoryBot'
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_comment(text, *)
|
36
|
+
return unless /sloth:(?<directive>disable|enable)/ =~ text
|
37
|
+
|
38
|
+
directive == 'disable' &&
|
39
|
+
@calls.reject! { |obj| obj[1] == lineno } ||
|
40
|
+
(@lines ||= @code.lines)[lineno - 1].match?(/^\s*#/) &&
|
41
|
+
(@disabled = directive != 'enable')
|
26
42
|
end
|
27
43
|
|
28
44
|
def on_fcall(loc, *)
|
29
|
-
|
45
|
+
store_call(loc)
|
30
46
|
end
|
31
47
|
|
32
48
|
def on_vcall(loc, *)
|
33
|
-
|
49
|
+
store_call(loc)
|
34
50
|
end
|
35
51
|
end
|
@@ -1,13 +1,13 @@
|
|
1
|
-
require '
|
1
|
+
require 'tmpdir'
|
2
2
|
|
3
3
|
module FactorySloth::SpecRunner
|
4
|
-
def self.call(spec_code, line: nil)
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
4
|
+
def self.call(spec_path, spec_code, line: nil)
|
5
|
+
Dir.mktmpdir do |tmpdir|
|
6
|
+
path = File.join(tmpdir, spec_path)
|
7
|
+
FileUtils.mkdir_p(File.dirname(path))
|
8
|
+
File.write(path, spec_code)
|
9
|
+
path_arg = [path, line].compact.map(&:to_s).join(':')
|
10
|
+
!!system("bundle exec rspec #{path_arg} --fail-fast 1>/dev/null 2>&1")
|
11
|
+
end
|
12
12
|
end
|
13
13
|
end
|
data/lib/factory_sloth.rb
CHANGED
@@ -2,6 +2,7 @@ module FactorySloth; end
|
|
2
2
|
|
3
3
|
require_relative 'factory_sloth/cli'
|
4
4
|
require_relative 'factory_sloth/code_mod'
|
5
|
+
require_relative 'factory_sloth/create_call'
|
5
6
|
require_relative 'factory_sloth/create_call_finder'
|
6
7
|
require_relative 'factory_sloth/done_tracker'
|
7
8
|
require_relative 'factory_sloth/file_processor'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: factory_sloth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Janosch Müller
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-05-
|
11
|
+
date: 2023-05-17 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -28,6 +28,7 @@ files:
|
|
28
28
|
- lib/factory_sloth.rb
|
29
29
|
- lib/factory_sloth/cli.rb
|
30
30
|
- lib/factory_sloth/code_mod.rb
|
31
|
+
- lib/factory_sloth/create_call.rb
|
31
32
|
- lib/factory_sloth/create_call_finder.rb
|
32
33
|
- lib/factory_sloth/done_tracker.rb
|
33
34
|
- lib/factory_sloth/file_processor.rb
|