factory_sloth 1.2.1 → 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 +7 -0
- data/README.md +12 -6
- data/lib/factory_sloth/code_mod.rb +44 -15
- data/lib/factory_sloth/done_tracker.rb +2 -0
- data/lib/factory_sloth/spec_runner.rb +15 -6
- data/lib/factory_sloth/version.rb +1 -1
- metadata +2 -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
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 ...
|
@@ -58,15 +58,21 @@ The `conflict` case is rare. It only happens if individual examples were green a
|
|
58
58
|
|
59
59
|
## Limitations / known issues
|
60
60
|
|
61
|
-
|
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
62
|
|
63
63
|
```ruby
|
64
|
-
|
65
|
-
|
66
|
-
expect(User.
|
64
|
+
user = create(:user)
|
65
|
+
User.delete_all
|
66
|
+
expect(User.count).to eq 0
|
67
67
|
```
|
68
68
|
|
69
|
-
This test will still pass if `
|
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 :)
|
70
76
|
|
71
77
|
## Development
|
72
78
|
|
@@ -19,13 +19,16 @@ class FactorySloth::CodeMod
|
|
19
19
|
# rspec call. However, this would make it impossible to use `--fail-fast`,
|
20
20
|
# and might make examples fail that are not as idempotent as they should be.
|
21
21
|
self.changed_create_calls =
|
22
|
-
create_calls
|
23
|
-
|
24
|
-
|
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
|
25
28
|
|
26
29
|
# validate whole spec after changes, e.g. to detect side-effects
|
27
30
|
self.ok = changed_create_calls.none? ||
|
28
|
-
FactorySloth::SpecRunner.call(path, patched_code)
|
31
|
+
FactorySloth::SpecRunner.call(path, patched_code).success?
|
29
32
|
changed_create_calls.clear unless ok?
|
30
33
|
patched_code.replace(original_code) unless ok?
|
31
34
|
end
|
@@ -56,30 +59,56 @@ class FactorySloth::CodeMod
|
|
56
59
|
/\A(?:.*\n){#{call.line - 1}}.{#{call.column}}\K#{call.name}/,
|
57
60
|
variant
|
58
61
|
)
|
59
|
-
checked_patched_code =
|
60
|
-
|
62
|
+
checked_patched_code = new_patched_code + checks(call.line, variant)
|
63
|
+
|
64
|
+
result = FactorySloth::SpecRunner.call(path, checked_patched_code, line: call.line)
|
65
|
+
if result.success?
|
61
66
|
puts "- #{call.name} in line #{call.line} can be replaced with #{variant}"
|
62
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
|
63
74
|
end
|
64
75
|
end
|
65
76
|
|
66
|
-
|
67
|
-
|
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
|
68
91
|
; defined?(FactoryBot) && defined?(RSpec) && RSpec.configure do |config|
|
69
|
-
|
92
|
+
records_by_line = {} # track records initialized through factories per line
|
70
93
|
|
71
94
|
FactoryBot::Syntax::Methods.class_eval do
|
72
|
-
alias ___original_#{variant} #{variant}
|
95
|
+
alias ___original_#{variant} #{variant} # e.g. ___original_build build
|
73
96
|
|
74
|
-
define_method("#{variant}") do |*args, **kwargs, &blk|
|
75
|
-
|
76
|
-
|
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
|
77
102
|
end
|
78
103
|
end
|
79
104
|
|
80
105
|
config.after(:suite) do
|
81
|
-
|
82
|
-
|
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
|
83
112
|
end
|
84
113
|
end
|
85
114
|
RUBY
|
@@ -1,13 +1,22 @@
|
|
1
|
+
require 'open3'
|
1
2
|
require 'tmpdir'
|
2
3
|
|
3
4
|
module FactorySloth::SpecRunner
|
4
5
|
def self.call(spec_path, spec_code, line: nil)
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
11
20
|
end
|
12
21
|
end
|
13
22
|
end
|
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:
|