factory_sloth 1.2.1 → 1.2.2
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 +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:
|