factory_sloth 1.2.0 → 1.2.1

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: 3550aaf57e03aa0da8d68264bab3c4a0a17ed264995859f34cc641e7e376b51b
4
- data.tar.gz: d947f5e3533b24b7740d509b5e9b8f3a832a6c535eb81f859ba7c562127c3e5a
3
+ metadata.gz: ce07258dd3b7b7e17ba54accb86be394e97f3f7c986b7d05705dcd54b93e6d50
4
+ data.tar.gz: 3160fc1436b8f9271f94b005a367c18084298c003623ca97bb773c9b10656f32
5
5
  SHA512:
6
- metadata.gz: e9abcbc30c854da1ec58f4ab292ab6ed9a851dcbd11bd00291b2902bdec14ec79c21145033c2a984b7243831dd27b426bf5e33f6e9d22272d673a9ff2f61dd97
7
- data.tar.gz: '03280bc6477e646583c5af49d46e8bacbf611387181c3e0625e007c019170b697151a78fbd1698a859358a512f483a9949f3b6b27006fe92fc85fd10ccb2b149'
6
+ metadata.gz: 44dec49c5e35641d9a92bf175bed7afa4a9a58a87e4c663fb50545aaec7551ee49ca88e61d827053c0505d0d0ae9e59d323a7eba3d059f7361d9cc610ba9cf08
7
+ data.tar.gz: 0ada9a7c4f0a787bef23d67d4bdcb1dbf80b22294b13467ccdf1b0ed0f044017cac9ffe2e2db4b61622f0da19996b3d4f6d733947fd1f1b3b45e6283ae773abf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.2.1] - 2023-05-17
4
+
5
+ ### Fixed
6
+
7
+ - Fixed handling of path-based, derived spec metadata
8
+ - Thanks to https://github.com/bquorning
9
+ - Fixed unnecessary spec runs for files without create calls or changes
10
+ - Fixed modification of factory calls that never run (e.g. in skipped examples)
11
+
3
12
  ## [1.2.0] - 2023-05-16
4
13
 
5
14
  ### Added
data/README.md CHANGED
@@ -56,13 +56,17 @@ 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
- - only works with RSpec so far
62
- - downgrades create calls that never run (e.g. in skipped examples)
63
- - downgrades create calls that are only checked for an absence of effects
64
- - e.g. `a = create(:a); b = create(:b); expect(Record.filtered).to eq [b]`
65
- - `# sloth:disable` / `# sloth:enable` comments can be used to control this
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.:
62
+
63
+ ```ruby
64
+ user1 = create(:user, in_search: true)
65
+ user2 = create(:user, in_search: false)
66
+ expect(User.searchable).to eq(user1)
67
+ ```
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 :)
66
70
 
67
71
  ## Development
68
72
 
@@ -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,15 @@ 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 { |line, col| [-line, -col] }.select do |line, col|
22
- try_patch(line, col, 'build') || try_patch(line, col, 'build_stubbed')
23
- end.sort
22
+ create_calls
23
+ .sort_by { |call| [-call.line, -call.column] }
24
+ .select { |call| try_patch(call, 'build') || try_patch(call, 'build_stubbed') }
24
25
 
25
26
  # validate whole spec after changes, e.g. to detect side-effects
26
- self.ok = FactorySloth::SpecRunner.call(patched_code)
27
- self.changed_create_calls.clear unless ok?
28
- self.patched_code = original_code unless ok?
27
+ self.ok = changed_create_calls.none? ||
28
+ FactorySloth::SpecRunner.call(path, patched_code)
29
+ changed_create_calls.clear unless ok?
30
+ patched_code.replace(original_code) unless ok?
29
31
  end
30
32
 
31
33
  def ok?
@@ -46,14 +48,40 @@ class FactorySloth::CodeMod
46
48
 
47
49
  private
48
50
 
49
- attr_writer :create_calls, :changed_create_calls, :ok, :original_code, :patched_code
51
+ attr_writer :create_calls, :changed_create_calls, :ok, :path, :original_code, :patched_code
50
52
 
51
- def try_patch(line, col, variant)
52
- new_patched_code =
53
- patched_code.sub(/\A(?:.*\n){#{line - 1}}.{#{col}}\Kcreate/, variant)
54
- if FactorySloth::SpecRunner.call(new_patched_code, line: line)
55
- puts "- create in line #{line} can be replaced with #{variant}"
53
+ def try_patch(call, base_variant)
54
+ variant = call.name.sub('create', base_variant)
55
+ new_patched_code = patched_code.sub(
56
+ /\A(?:.*\n){#{call.line - 1}}.{#{call.column}}\K#{call.name}/,
57
+ variant
58
+ )
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)
61
+ puts "- #{call.name} in line #{call.line} can be replaced with #{variant}"
56
62
  self.patched_code = new_patched_code
57
63
  end
58
64
  end
65
+
66
+ def with_execution_check(spec_code, line, variant)
67
+ spec_code + <<~RUBY
68
+ ; defined?(FactoryBot) && defined?(RSpec) && RSpec.configure do |config|
69
+ executed_lines = []
70
+
71
+ FactoryBot::Syntax::Methods.class_eval do
72
+ alias ___original_#{variant} #{variant}
73
+
74
+ define_method("#{variant}") do |*args, **kwargs, &blk|
75
+ executed_lines << caller_locations(1, 1)&.first&.lineno
76
+ ___original_#{variant}(*args, **kwargs, &blk)
77
+ end
78
+ end
79
+
80
+ config.after(:suite) do
81
+ executed_lines.include?(#{line}) ||
82
+ fail("unused factory in line #{line} - will not be modified")
83
+ end
84
+ end
85
+ RUBY
86
+ end
59
87
  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 :locations
4
+ attr_reader :calls
5
5
 
6
6
  def self.call(code:)
7
- new(code).tap(&:parse).locations
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
- @locations = []
14
+ @calls = []
15
15
  end
16
16
  private_class_method :new
17
17
 
18
- def store_location(loc)
19
- @locations << loc if loc.instance_of?(Array) && !@disabled
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
- [lineno, column] if %w[create create_list create_pair].include?(name)
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, _, loc, *)
27
- store_location(loc) if mod == 'FactoryBot'
27
+ def on_call(mod, _, obj, *)
28
+ store_call(obj) if mod == 'FactoryBot'
28
29
  end
29
30
 
30
- def on_command_call(mod, _, loc, *)
31
- store_location(loc) if mod == 'FactoryBot'
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
- @locations.reject! { |loc| loc.first == lineno } ||
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
- store_location(loc)
45
+ store_call(loc)
45
46
  end
46
47
 
47
48
  def on_vcall(loc, *)
48
- store_location(loc)
49
+ store_call(loc)
49
50
  end
50
51
  end
@@ -21,7 +21,7 @@ module FactorySloth
21
21
 
22
22
  def process(path, dry_run:)
23
23
  code = File.read(path)
24
- result = CodeMod.call(code)
24
+ result = CodeMod.call(path, code)
25
25
  unless dry_run
26
26
  File.write(path, result.patched_code) if result.changed?
27
27
  end
@@ -1,13 +1,13 @@
1
- require 'tempfile'
1
+ require 'tmpdir'
2
2
 
3
3
  module FactorySloth::SpecRunner
4
- def self.call(spec_code, line: nil)
5
- tempfile = Tempfile.new
6
- tempfile.write(spec_code)
7
- tempfile.close
8
- path = [tempfile.path, line].compact.map(&:to_s).join(':')
9
- result = !!system("bundle exec rspec #{path} --fail-fast 1>/dev/null 2>&1")
10
- tempfile.unlink
11
- result
4
+ 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")
11
+ end
12
12
  end
13
13
  end
@@ -1,3 +1,3 @@
1
1
  module FactorySloth
2
- VERSION = '1.2.0'
2
+ VERSION = '1.2.1'
3
3
  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.0
4
+ version: 1.2.1
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-16 00:00:00.000000000 Z
11
+ date: 2023-05-17 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