factory_sloth 1.2.0 → 1.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +17 -7
- data/lib/factory_sloth/code_mod.rb +73 -16
- data/lib/factory_sloth/create_call.rb +1 -0
- data/lib/factory_sloth/create_call_finder.rb +14 -13
- data/lib/factory_sloth/done_tracker.rb +2 -0
- data/lib/factory_sloth/file_processor.rb +1 -1
- data/lib/factory_sloth/spec_runner.rb +18 -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: c628e26d703d50de9a6f087174d01132942eb557ccbb4519a173116922cb2541
|
4
|
+
data.tar.gz: 40c2d6873fb446d5a8dbba7339ebc481e766989ff722531e24d891213bac3434
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa04cdbe7e3bf0fe25b9754f6fa5d356db63c4cf18ef193c5a0b11ae8629c36cd3e39ffda5fa71e47cf1ddef16b0dc4b84564c87cc6e493169150bbe35aeccd3
|
7
|
+
data.tar.gz: 8bfc7521d09398337217f0ed04adccda7380f653efd46c2c0d39858082d7d949996142b5bf074eaab36524b3df197a29875b72bd21a8b7a326886303cbd1d9fa
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.2.2] - 2023-05-18
|
4
|
+
|
5
|
+
### Fixed
|
6
|
+
|
7
|
+
- No longer changes create to build for records that are persisted later
|
8
|
+
- Fixed duplicate entries in `.factory_sloth_done` file
|
9
|
+
|
10
|
+
## [1.2.1] - 2023-05-17
|
11
|
+
|
12
|
+
### Fixed
|
13
|
+
|
14
|
+
- Fixed handling of path-based, derived spec metadata
|
15
|
+
- Thanks to https://github.com/bquorning
|
16
|
+
- Fixed unnecessary spec runs for files without create calls or changes
|
17
|
+
- Fixed modification of factory calls that never run (e.g. in skipped examples)
|
18
|
+
|
3
19
|
## [1.2.0] - 2023-05-16
|
4
20
|
|
5
21
|
### Added
|
data/README.md
CHANGED
@@ -44,7 +44,7 @@ Processing spec/lib/string_ext_spec.rb ...
|
|
44
44
|
|
45
45
|
Processing spec/models/user_spec.rb ...
|
46
46
|
- create in line 3 can be replaced with build
|
47
|
-
-
|
47
|
+
- create_list in line 4 can be replaced with build_list
|
48
48
|
🟢 3 create calls found, 2 replaced
|
49
49
|
|
50
50
|
Processing spec/weird_dir/crazy_spec.rb ...
|
@@ -56,13 +56,23 @@ 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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
61
|
+
`factory_sloth` only works with RSpec so far. It also works best with unit tests such as model specs. It 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
|
+
user = create(:user)
|
65
|
+
User.delete_all
|
66
|
+
expect(User.count).to eq 0
|
67
|
+
```
|
68
|
+
|
69
|
+
This test will still pass if `user` is never inserted into the database in the first place, leading `factory_sloth` to believe that `build` suffices here. However, this change makes the test no longer assert the same thing and reduces coverage. Magic comments can be used to prevent `factory_sloth` from making such changes. `factory_sloth` will not touch lines with inline `# sloth:disable` comments, or sections framed in `# sloth:disable` / `# sloth:enable` comments. Another option is to write the test in a different (and arguably more assertive) way, e.g.:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
expect { User.delete_all }.to change { User.count }.from(1).to(0)
|
73
|
+
```
|
74
|
+
|
75
|
+
If you have a good idea about how to detect such cases automatically, let me know :)
|
66
76
|
|
67
77
|
## Development
|
68
78
|
|
@@ -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,18 @@ 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.sort_by { |
|
22
|
-
|
23
|
-
|
22
|
+
create_calls.sort_by { |call| [-call.line, -call.column] }.select do |call|
|
23
|
+
build_result = try_patch(call, 'build')
|
24
|
+
next if build_result == ABORT
|
25
|
+
|
26
|
+
build_result == SUCCESS || try_patch(call, 'build_stubbed') == SUCCESS
|
27
|
+
end
|
24
28
|
|
25
29
|
# validate whole spec after changes, e.g. to detect side-effects
|
26
|
-
self.ok =
|
27
|
-
|
28
|
-
|
30
|
+
self.ok = changed_create_calls.none? ||
|
31
|
+
FactorySloth::SpecRunner.call(path, patched_code).success?
|
32
|
+
changed_create_calls.clear unless ok?
|
33
|
+
patched_code.replace(original_code) unless ok?
|
29
34
|
end
|
30
35
|
|
31
36
|
def ok?
|
@@ -46,14 +51,66 @@ class FactorySloth::CodeMod
|
|
46
51
|
|
47
52
|
private
|
48
53
|
|
49
|
-
attr_writer :create_calls, :changed_create_calls, :ok, :original_code, :patched_code
|
54
|
+
attr_writer :create_calls, :changed_create_calls, :ok, :path, :original_code, :patched_code
|
55
|
+
|
56
|
+
def try_patch(call, base_variant)
|
57
|
+
variant = call.name.sub('create', base_variant)
|
58
|
+
new_patched_code = patched_code.sub(
|
59
|
+
/\A(?:.*\n){#{call.line - 1}}.{#{call.column}}\K#{call.name}/,
|
60
|
+
variant
|
61
|
+
)
|
62
|
+
checked_patched_code = new_patched_code + checks(call.line, variant)
|
50
63
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
if FactorySloth::SpecRunner.call(new_patched_code, line: line)
|
55
|
-
puts "- create in line #{line} can be replaced with #{variant}"
|
64
|
+
result = FactorySloth::SpecRunner.call(path, checked_patched_code, line: call.line)
|
65
|
+
if result.success?
|
66
|
+
puts "- #{call.name} in line #{call.line} can be replaced with #{variant}"
|
56
67
|
self.patched_code = new_patched_code
|
68
|
+
SUCCESS
|
69
|
+
elsif result.exitstatus == FACTORY_UNUSED_CODE
|
70
|
+
puts "- #{call.name} in line #{call.line} is never executed, skipping"
|
71
|
+
ABORT
|
72
|
+
elsif result.exitstatus == FACTORY_PERSISTED_LATER_CODE
|
73
|
+
ABORT
|
57
74
|
end
|
58
75
|
end
|
76
|
+
|
77
|
+
ABORT = :ABORT # returned if there is no need to try other variants
|
78
|
+
SUCCESS = :SUCCESS
|
79
|
+
|
80
|
+
FACTORY_UNUSED_CODE = 77
|
81
|
+
FACTORY_PERSISTED_LATER_CODE = 78
|
82
|
+
|
83
|
+
# This adds code that makes a spec run fail and thus prevents changes if:
|
84
|
+
# a) the patched factory in the given line is never called
|
85
|
+
# b) the built record was persisted later anyway
|
86
|
+
# The rationale behind a) is that things like skipped examples should not
|
87
|
+
# be broken. The rationale behind b) is that not much DB work would be saved,
|
88
|
+
# but diff noise would be increased and ease of editing the example reduced.
|
89
|
+
def checks(line, variant)
|
90
|
+
<<~RUBY
|
91
|
+
; defined?(FactoryBot) && defined?(RSpec) && RSpec.configure do |config|
|
92
|
+
records_by_line = {} # track records initialized through factories per line
|
93
|
+
|
94
|
+
FactoryBot::Syntax::Methods.class_eval do
|
95
|
+
alias ___original_#{variant} #{variant} # e.g. ___original_build build
|
96
|
+
|
97
|
+
define_method("#{variant}") do |*args, **kwargs, &blk| # e.g. build
|
98
|
+
result = ___original_#{variant}(*args, **kwargs, &blk)
|
99
|
+
list = records_by_line[caller_locations(1, 1)&.first&.lineno] ||= []
|
100
|
+
list.concat([result].flatten) # to work with single, list, and pair
|
101
|
+
result
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
config.after(:suite) do
|
106
|
+
records = records_by_line[#{line}]
|
107
|
+
records&.any? || exit!(#{FACTORY_UNUSED_CODE})
|
108
|
+
unless "#{variant}".include?('stub') # factory_bot stub stubs persisted? as true
|
109
|
+
records.any? { |r| r.respond_to?(:persisted?) && r.persisted? } &&
|
110
|
+
exit!(#{FACTORY_PERSISTED_LATER_CODE})
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
RUBY
|
115
|
+
end
|
59
116
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
FactorySloth::CreateCall = Struct.new(:name, :line, :column, keyword_init: true)
|
@@ -1,50 +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
10
|
def initialize(code, ...)
|
11
11
|
super
|
12
12
|
@code = code
|
13
13
|
@disabled = false
|
14
|
-
@
|
14
|
+
@calls = []
|
15
15
|
end
|
16
16
|
private_class_method :new
|
17
17
|
|
18
|
-
def
|
19
|
-
@
|
18
|
+
def store_call(obj)
|
19
|
+
@calls << obj if obj.is_a?(FactorySloth::CreateCall) && !@disabled
|
20
20
|
end
|
21
21
|
|
22
22
|
def on_ident(name, *)
|
23
|
-
|
23
|
+
%w[create create_list create_pair].include?(name) &&
|
24
|
+
FactorySloth::CreateCall.new(name: name, line: lineno, column: column)
|
24
25
|
end
|
25
26
|
|
26
|
-
def on_call(mod, _,
|
27
|
-
|
27
|
+
def on_call(mod, _, obj, *)
|
28
|
+
store_call(obj) if mod == 'FactoryBot'
|
28
29
|
end
|
29
30
|
|
30
|
-
def on_command_call(mod, _,
|
31
|
-
|
31
|
+
def on_command_call(mod, _, obj, *)
|
32
|
+
store_call(obj) if mod == 'FactoryBot'
|
32
33
|
end
|
33
34
|
|
34
35
|
def on_comment(text, *)
|
35
36
|
return unless /sloth:(?<directive>disable|enable)/ =~ text
|
36
37
|
|
37
38
|
directive == 'disable' &&
|
38
|
-
@
|
39
|
+
@calls.reject! { |obj| obj[1] == lineno } ||
|
39
40
|
(@lines ||= @code.lines)[lineno - 1].match?(/^\s*#/) &&
|
40
41
|
(@disabled = directive != 'enable')
|
41
42
|
end
|
42
43
|
|
43
44
|
def on_fcall(loc, *)
|
44
|
-
|
45
|
+
store_call(loc)
|
45
46
|
end
|
46
47
|
|
47
48
|
def on_vcall(loc, *)
|
48
|
-
|
49
|
+
store_call(loc)
|
49
50
|
end
|
50
51
|
end
|
@@ -1,13 +1,22 @@
|
|
1
|
-
require '
|
1
|
+
require 'open3'
|
2
|
+
require 'tmpdir'
|
2
3
|
|
3
4
|
module FactorySloth::SpecRunner
|
4
|
-
def self.call(spec_code, line: nil)
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
5
|
+
def self.call(spec_path, spec_code, line: nil)
|
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
|
+
command = "bundle exec rspec #{path_arg} --fail-fast --order defined 2>&1"
|
11
|
+
_output, status = Open3.capture2(command)
|
12
|
+
status
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.tmpdir
|
16
|
+
@tmpdir ||= begin
|
17
|
+
dir = Dir.mktmpdir('factory_sloth-')
|
18
|
+
at_exit { FileUtils.remove_entry(dir) if File.exist?(dir) }
|
19
|
+
dir
|
20
|
+
end
|
12
21
|
end
|
13
22
|
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.2.
|
4
|
+
version: 1.2.2
|
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-19 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
|