factory_sloth 1.1.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: cefc242e547927b9e8805b2dd4be60905f4dd5e484a5b42da6699e3c104879e8
4
- data.tar.gz: 60c06cc54a40a35b4a72850d636510af0883071760fd88b5587091e1866de261
3
+ metadata.gz: ce07258dd3b7b7e17ba54accb86be394e97f3f7c986b7d05705dcd54b93e6d50
4
+ data.tar.gz: 3160fc1436b8f9271f94b005a367c18084298c003623ca97bb773c9b10656f32
5
5
  SHA512:
6
- metadata.gz: c96fcd9841cd70fc952f6f503bd2e84024a2bf786fd132805f0badff7b5208f720be9cc36a73981d77f65fa932e256d7b4ecf2c08e59425ce811e10c61f2813a
7
- data.tar.gz: 885c2928964f3dff1e041a4fa09b3c85a13a452e3168a6985754b0ec6fae01c1cc550001f76808451d02ee477bdcb82b1d2c10dd82576f95b1555b2eab325dcb
6
+ metadata.gz: 44dec49c5e35641d9a92bf175bed7afa4a9a58a87e4c663fb50545aaec7551ee49ca88e61d827053c0505d0d0ae9e59d323a7eba3d059f7361d9cc610ba9cf08
7
+ data.tar.gz: 0ada9a7c4f0a787bef23d67d4bdcb1dbf80b22294b13467ccdf1b0ed0f044017cac9ffe2e2db4b61622f0da19996b3d4f6d733947fd1f1b3b45e6283ae773abf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
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
+
12
+ ## [1.2.0] - 2023-05-16
13
+
14
+ ### Added
15
+
16
+ - Added support for magic comments `# sloth:disable`, `# sloth:enable`
17
+
3
18
  ## [1.1.0] - 2023-05-16
4
19
 
5
20
  ### Added
data/README.md CHANGED
@@ -56,10 +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)
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 :)
63
70
 
64
71
  ## Development
65
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,35 +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
- def initialize(...)
10
+ def initialize(code, ...)
11
11
  super
12
- @locations = []
12
+ @code = code
13
+ @disabled = false
14
+ @calls = []
13
15
  end
14
16
  private_class_method :new
15
17
 
18
+ def store_call(obj)
19
+ @calls << obj if obj.is_a?(FactorySloth::CreateCall) && !@disabled
20
+ end
21
+
16
22
  def on_ident(name, *)
17
- [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)
18
25
  end
19
26
 
20
- def on_call(mod, _, loc, *)
21
- @locations << loc if loc.instance_of?(Array) && mod == 'FactoryBot'
27
+ def on_call(mod, _, obj, *)
28
+ store_call(obj) if mod == 'FactoryBot'
22
29
  end
23
30
 
24
- def on_command_call(mod, _, loc, *)
25
- @locations << loc if loc.instance_of?(Array) && mod == 'FactoryBot'
31
+ def on_command_call(mod, _, obj, *)
32
+ store_call(obj) if mod == 'FactoryBot'
33
+ end
34
+
35
+ def on_comment(text, *)
36
+ return unless /sloth:(?<directive>disable|enable)/ =~ text
37
+
38
+ directive == 'disable' &&
39
+ @calls.reject! { |obj| obj[1] == lineno } ||
40
+ (@lines ||= @code.lines)[lineno - 1].match?(/^\s*#/) &&
41
+ (@disabled = directive != 'enable')
26
42
  end
27
43
 
28
44
  def on_fcall(loc, *)
29
- @locations << loc if loc.instance_of?(Array)
45
+ store_call(loc)
30
46
  end
31
47
 
32
48
  def on_vcall(loc, *)
33
- @locations << loc if loc.instance_of?(Array)
49
+ store_call(loc)
34
50
  end
35
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.1.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.1.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