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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ce07258dd3b7b7e17ba54accb86be394e97f3f7c986b7d05705dcd54b93e6d50
4
- data.tar.gz: 3160fc1436b8f9271f94b005a367c18084298c003623ca97bb773c9b10656f32
3
+ metadata.gz: c628e26d703d50de9a6f087174d01132942eb557ccbb4519a173116922cb2541
4
+ data.tar.gz: 40c2d6873fb446d5a8dbba7339ebc481e766989ff722531e24d891213bac3434
5
5
  SHA512:
6
- metadata.gz: 44dec49c5e35641d9a92bf175bed7afa4a9a58a87e4c663fb50545aaec7551ee49ca88e61d827053c0505d0d0ae9e59d323a7eba3d059f7361d9cc610ba9cf08
7
- data.tar.gz: 0ada9a7c4f0a787bef23d67d4bdcb1dbf80b22294b13467ccdf1b0ed0f044017cac9ffe2e2db4b61622f0da19996b3d4f6d733947fd1f1b3b45e6283ae773abf
6
+ metadata.gz: aa04cdbe7e3bf0fe25b9754f6fa5d356db63c4cf18ef193c5a0b11ae8629c36cd3e39ffda5fa71e47cf1ddef16b0dc4b84564c87cc6e493169150bbe35aeccd3
7
+ data.tar.gz: 8bfc7521d09398337217f0ed04adccda7380f653efd46c2c0d39858082d7d949996142b5bf074eaab36524b3df197a29875b72bd21a8b7a326886303cbd1d9fa
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## [1.2.1] - 2023-05-17
4
11
 
5
12
  ### Fixed
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
- - create in line 4 can be replaced with build
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
- 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.:
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
- user1 = create(:user, in_search: true)
65
- user2 = create(:user, in_search: false)
66
- expect(User.searchable).to eq(user1)
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 `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 :)
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
- .sort_by { |call| [-call.line, -call.column] }
24
- .select { |call| try_patch(call, 'build') || try_patch(call, 'build_stubbed') }
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 = with_execution_check(new_patched_code, call.line, variant)
60
- if FactorySloth::SpecRunner.call(path, checked_patched_code, line: call.line)
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
- def with_execution_check(spec_code, line, variant)
67
- spec_code + <<~RUBY
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
- executed_lines = []
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
- executed_lines << caller_locations(1, 1)&.first&.lineno
76
- ___original_#{variant}(*args, **kwargs, &blk)
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
- executed_lines.include?(#{line}) ||
82
- fail("unused factory in line #{line} - will not be modified")
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
@@ -7,6 +7,8 @@ module FactorySloth::DoneTracker
7
7
 
8
8
  def mark_as_done(path)
9
9
  normalized_path = normalize(path)
10
+ return if done?(normalized_path)
11
+
10
12
  done << normalized_path
11
13
  File.open(file, 'a') { |f| f.puts(normalized_path) }
12
14
  end
@@ -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
- 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")
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
@@ -1,3 +1,3 @@
1
1
  module FactorySloth
2
- VERSION = '1.2.1'
2
+ VERSION = '1.2.2'
3
3
  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.1
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-17 00:00:00.000000000 Z
11
+ date: 2023-05-19 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: