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 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: