factory_sloth 1.2.2 β†’ 1.3.0

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: c628e26d703d50de9a6f087174d01132942eb557ccbb4519a173116922cb2541
4
- data.tar.gz: 40c2d6873fb446d5a8dbba7339ebc481e766989ff722531e24d891213bac3434
3
+ metadata.gz: 9032b54abec61f25adcfd938a503f312017667f64b4aa1f303ede50a346a62be
4
+ data.tar.gz: 901d358d3572a708ec8190c2d2e59dbdab3bce42f894301284dc285895d48f19
5
5
  SHA512:
6
- metadata.gz: aa04cdbe7e3bf0fe25b9754f6fa5d356db63c4cf18ef193c5a0b11ae8629c36cd3e39ffda5fa71e47cf1ddef16b0dc4b84564c87cc6e493169150bbe35aeccd3
7
- data.tar.gz: 8bfc7521d09398337217f0ed04adccda7380f653efd46c2c0d39858082d7d949996142b5bf074eaab36524b3df197a29875b72bd21a8b7a326886303cbd1d9fa
6
+ metadata.gz: b1aa29c8c7dd007c5e82ee4cfbc164df1550ace4c2d7880e0471b4a892a2dad33a8e03469c56ec5620f99e7c2dc8c6c47f0d7256102910385acea9a3f97f4b74
7
+ data.tar.gz: bace623884d7e447200af29fb216f8814f86a7bae4deda093105bb9df716c8151332136a473c31cd6c9e51bc07ded9f2306e9eda29b536b7b4aa130384f0059a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.0] - 2023-05-22
4
+
5
+ ### Added
6
+
7
+ - nicer output
8
+ - verbose mode
9
+
3
10
  ## [1.2.2] - 2023-05-18
4
11
 
5
12
  ### Fixed
data/README.md CHANGED
@@ -27,6 +27,7 @@ Examples:
27
27
  Options:
28
28
  -f, --force Ignore ./.factory_sloth_done
29
29
  -l, --lint Dont fix, just list bad create calls
30
+ -V, --verbose Verbose output, useful for debugging
30
31
  -v, --version Show gem version
31
32
  -h, --help Show this help
32
33
  ```
@@ -36,20 +37,25 @@ Options:
36
37
  While running, `factory_sloth` produces output like this:
37
38
 
38
39
  ```
39
- Processing spec/features/sign_up_spec.rb ...
40
- 🟑 2 create calls found, 0 replaced
40
+ 🟑 spec/features/sign_up_spec.rb: 2 create calls found, 0 replaced
41
41
 
42
- Processing spec/lib/string_ext_spec.rb ...
43
- βšͺ️ 0 create calls found, 0 replaced
42
+ βšͺ️ spec/lib/string_ext_spec.rb: 0 create calls found, 0 replaced
44
43
 
45
- Processing spec/models/user_spec.rb ...
46
- - create in line 3 can be replaced with build
47
- - create_list in line 4 can be replaced with build_list
48
- 🟒 3 create calls found, 2 replaced
44
+ spec/models/user_spec.rb:3:2: create replaced with build
45
+ expect(create(:user)).not_to be_nil
46
+ ^^^^^^
49
47
 
50
- Processing spec/weird_dir/crazy_spec.rb ...
51
- - create in line 8 can be replaced with build_stubbed
52
- πŸ”΄ 33 create calls found, 0 replaced (conflict)
48
+ spec/models/user_spec.rb:4:2: create_list replaced with build_list
49
+ expect(create_list(:user, 2).count).to eq 2
50
+ ^^^^^^^^^^^
51
+
52
+ 🟒 spec/models/user_spec.rb: 3 create calls found, 2 replaced
53
+
54
+ spec/weird_dir/crazy_spec.rb:8:4: create replaced with build_stubbed
55
+ expect(create(:user)).not_to be_nil
56
+ ^^^^^^
57
+
58
+ πŸ”΄ spec/weird_dir/crazy_spec.rb: 33 create calls found, 0 replaced (conflict)
53
59
 
54
60
  Scanned 4 files, found 2 unnecessary create calls across 1 files and 1 broken specs
55
61
  ```
@@ -7,8 +7,7 @@ module FactorySloth
7
7
  def call(argv = ARGV)
8
8
  args = option_parser.parse!(argv)
9
9
  specs = SpecPicker.call(paths: args)
10
- forced_files = @force ? specs : args
11
- results = FileProcessor.call(files: specs, forced_files: forced_files, dry_run: @lint)
10
+ results = FileProcessor.call(files: specs, forced_files: args)
12
11
  print_summary(results)
13
12
  end
14
13
 
@@ -29,11 +28,16 @@ module FactorySloth
29
28
  opts.separator 'Options:'
30
29
 
31
30
  opts.on('-f', '--force', "Ignore #{DoneTracker.file}") do
32
- @force = true
31
+ FactorySloth.force = true
33
32
  end
34
33
 
35
34
  opts.on('-l', '--lint', 'Dont fix, just list bad create calls') do
36
- @lint = true
35
+ FactorySloth.dry_run = true
36
+ FactorySloth.lint = true
37
+ end
38
+
39
+ opts.on('-V', '--verbose', 'Verbose output, useful for debugging') do
40
+ FactorySloth.verbose = true
37
41
  end
38
42
 
39
43
  opts.on('-v', '--version', 'Show gem version') do
@@ -49,14 +53,14 @@ module FactorySloth
49
53
  end
50
54
 
51
55
  def print_summary(results)
52
- unnecessary_call_count = results.values.sum { |v| v[:changed_create_calls].count }
53
- changed_specs = results.keys.select { |path| results[path][:changed_create_calls].any? }
56
+ change_sum = results.values.sum { |v| v[:change_count] }
57
+ changed_specs = results.keys.select { |path| results[path][:change_count] > 0 }
54
58
  broken_specs = results.keys.select { |path| !results[path][:ok] }
55
- stats = "Scanned #{results.count} files, found #{unnecessary_call_count}"\
59
+ stats = "Scanned #{results.count} files, found #{change_sum}"\
56
60
  " unnecessary create calls across #{changed_specs.count} files"\
57
61
  "#{" and #{broken_specs.count} broken specs" if broken_specs.any?}"
58
62
 
59
- if @lint && unnecessary_call_count > 0
63
+ if FactorySloth.lint && change_sum > 0
60
64
  warn "#{stats}:\n#{(changed_specs + broken_specs).join("\n")}"
61
65
  exit 1
62
66
  else
@@ -1,116 +1,117 @@
1
- class FactorySloth::CodeMod
2
- attr_reader :create_calls, :changed_create_calls, :path, :original_code, :patched_code
1
+ module FactorySloth
2
+ class CodeMod
3
+ attr_reader :create_calls, :changed_create_calls, :path, :original_code, :patched_code
3
4
 
4
- def self.call(path, code)
5
- new(path, code).tap(&:call)
6
- end
5
+ require 'forwardable'
6
+ extend Forwardable
7
7
 
8
- def initialize(path, code)
9
- self.path = path
10
- self.original_code = code
11
- self.patched_code = code
12
- end
8
+ def_delegator :changed_create_calls, :any?, :changed?
9
+ def_delegator :changed_create_calls, :count, :change_count
10
+ def_delegator :create_calls, :count, :create_count
11
+
12
+ def self.call(path, code)
13
+ new(path, code).tap(&:call)
14
+ end
15
+
16
+ def initialize(path, code)
17
+ self.path = path
18
+ self.original_code = code
19
+ self.patched_code = code
20
+ end
21
+
22
+ def call
23
+ self.create_calls = CreateCallFinder.call(code: original_code)
13
24
 
14
- def call
15
- self.create_calls = FactorySloth::CreateCallFinder.call(code: original_code)
25
+ # Performance note: it might be faster to write ALL possible patches for a
26
+ # given spec file to tempfiles first, and then run them all in a single
27
+ # rspec call. However, this would make it impossible to use `--fail-fast`,
28
+ # and might make examples fail that are not as idempotent as they should be.
29
+ self.changed_create_calls =
30
+ create_calls.sort_by { |call| [-call.line, -call.column] }.select do |call|
31
+ build_result = try_patch(call, 'build')
32
+ next if build_result == ABORT
16
33
 
17
- # Performance note: it might be faster to write ALL possible patches for a
18
- # given spec file to tempfiles first, and then run them all in a single
19
- # rspec call. However, this would make it impossible to use `--fail-fast`,
20
- # and might make examples fail that are not as idempotent as they should be.
21
- self.changed_create_calls =
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
34
+ build_result == SUCCESS || try_patch(call, 'build_stubbed') == SUCCESS
35
+ end
25
36
 
26
- build_result == SUCCESS || try_patch(call, 'build_stubbed') == SUCCESS
37
+ # validate whole spec after changes, e.g. to detect side-effects
38
+ self.ok = changed_create_calls.none? || begin
39
+ FactorySloth.verbose && puts("Checking whole file after changes")
40
+ run(patched_code).success?
27
41
  end
42
+ ok? || changed_create_calls.clear && patched_code.replace(original_code)
43
+ end
28
44
 
29
- # validate whole spec after changes, e.g. to detect side-effects
30
- self.ok = changed_create_calls.none? ||
31
- FactorySloth::SpecRunner.call(path, patched_code).success?
32
- changed_create_calls.clear unless ok?
33
- patched_code.replace(original_code) unless ok?
34
- end
45
+ def ok?
46
+ @ok
47
+ end
35
48
 
36
- def ok?
37
- @ok
38
- end
49
+ def message
50
+ stats = "#{path}: #{create_count} create calls found, #{change_count} "\
51
+ "#{FactorySloth.dry_run ? 'replaceable' : 'replaced'}"
39
52
 
40
- def changed?
41
- change_count > 0
42
- end
53
+ return "πŸ”΄ #{stats} (conflict)" unless ok?
43
54
 
44
- def create_count
45
- create_calls.count
46
- end
55
+ if create_count == 0
56
+ "βšͺ️ #{stats}"
57
+ elsif change_count == 0
58
+ "🟑 #{stats}"
59
+ else
60
+ "🟒 #{stats}"
61
+ end
62
+ end
47
63
 
48
- def change_count
49
- changed_create_calls.count
50
- end
64
+ private
65
+
66
+ attr_writer :create_calls, :changed_create_calls, :ok, :path, :original_code, :patched_code
67
+
68
+ def try_patch(call, base_variant)
69
+ variant = call.name.sub('create', base_variant)
70
+ FactorySloth.verbose && puts("#{link_to_call(call)}: trying #{variant} ...")
71
+
72
+ new_patched_code = patched_code.sub(
73
+ /\A(?:.*\R){#{call.line - 1}}.{#{call.column}}\K#{call.name}/,
74
+ variant
75
+ )
76
+ checked_patched_code = new_patched_code + ExecutionCheck.for(call.line, variant)
77
+
78
+ result = run(checked_patched_code, line: call.line)
79
+
80
+ if result.success?
81
+ info = FactorySloth.dry_run ? 'can be replaced' : 'replaced'
82
+ puts call_message(call, "#{info} with #{variant}"), ''
83
+ self.patched_code = new_patched_code
84
+ SUCCESS
85
+ elsif result.exitstatus == ExecutionCheck::FACTORY_UNUSED_CODE
86
+ puts call_message(call, "is never executed, skipping"), ''
87
+ ABORT
88
+ elsif result.exitstatus == ExecutionCheck::FACTORY_PERSISTED_LATER_CODE
89
+ FactorySloth.verbose && puts("Record is persisted later, skipping")
90
+ ABORT
91
+ end
92
+ end
51
93
 
52
- private
53
-
54
- attr_writer :create_calls, :changed_create_calls, :ok, :path, :original_code, :patched_code
55
-
56
- def try_patch(call, base_variant)
57
- variant = call.name.sub('create', base_variant)
58
- new_patched_code = patched_code.sub(
59
- /\A(?:.*\n){#{call.line - 1}}.{#{call.column}}\K#{call.name}/,
60
- variant
61
- )
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?
66
- puts "- #{call.name} in line #{call.line} can be replaced with #{variant}"
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
94
+ def run(code, line: nil)
95
+ result = SpecRunner.call(path, code, line: line)
96
+ FactorySloth.verbose && puts(' RSpec output:', result.output.gsub(/^/, ' '))
97
+ result
74
98
  end
75
- end
76
99
 
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
91
- ; defined?(FactoryBot) && defined?(RSpec) && RSpec.configure do |config|
92
- records_by_line = {} # track records initialized through factories per line
93
-
94
- FactoryBot::Syntax::Methods.class_eval do
95
- alias ___original_#{variant} #{variant} # e.g. ___original_build build
96
-
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
102
- end
103
- end
100
+ ABORT = :ABORT # returned if there is no need to try other variants
101
+ SUCCESS = :SUCCESS
104
102
 
105
- config.after(:suite) do
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
112
- end
113
- end
114
- RUBY
103
+ def call_message(call, message)
104
+ line_content = original_code[/\A(?:.*\R){#{call.line - 1}}\K.*/]
105
+ indent = line_content[/^\s*/]
106
+
107
+ "#{link_to_call(call)}: #{call.name} #{message}\n"\
108
+ " #{line_content.delete_prefix(indent)}\n"\
109
+ " #{' ' * (call.column - indent.size)}#{Color.yellow('^' * call.name.size)}"
110
+ end
111
+
112
+ def link_to_call(call)
113
+ # note: column from Ripper is 0-indexed, editors expect 1-indexed columns
114
+ Color.light_blue("#{path}:#{call.line}:#{call.column + 1}")
115
+ end
115
116
  end
116
117
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FactorySloth::Color
4
+ extend self
5
+
6
+ def yellow(str)
7
+ colorize(str, 33)
8
+ end
9
+
10
+ def light_blue(str)
11
+ colorize(str, 36)
12
+ end
13
+
14
+ private
15
+
16
+ def colorize(str, color_code)
17
+ return str unless $stdout.is_a?(IO) && $stdout.tty?
18
+
19
+ "\e[#{color_code}m#{str}\e[0m"
20
+ end
21
+ end
@@ -1 +1 @@
1
- FactorySloth::CreateCall = Struct.new(:name, :line, :column, keyword_init: true)
1
+ FactorySloth::CreateCall = Struct.new(:column, :line, :name, keyword_init: true)
@@ -0,0 +1,39 @@
1
+ # This adds code that makes a spec run fail and thus prevents changes if:
2
+ # a) the patched factory in the given line is never called
3
+ # b) the built record was persisted later anyway
4
+ # The rationale behind a) is that things like skipped examples should not
5
+ # be broken. The rationale behind b) is that not much DB work would be saved,
6
+ # but diff noise would be increased and ease of editing the example reduced.
7
+
8
+ module FactorySloth::ExecutionCheck
9
+ FACTORY_UNUSED_CODE = 77
10
+ FACTORY_PERSISTED_LATER_CODE = 78
11
+
12
+ def self.for(line, variant)
13
+ <<~RUBY
14
+ ; defined?(FactoryBot) && defined?(RSpec) && RSpec.configure do |config|
15
+ records_by_line = {} # track records initialized through factories per line
16
+
17
+ FactoryBot::Syntax::Methods.class_eval do
18
+ alias ___original_#{variant} #{variant} # e.g. ___original_build build
19
+
20
+ define_method("#{variant}") do |*args, **kwargs, &blk| # e.g. build
21
+ result = ___original_#{variant}(*args, **kwargs, &blk)
22
+ list = records_by_line[caller_locations(1, 1)&.first&.lineno] ||= []
23
+ list.concat([result].flatten) # to work with single, list, and pair
24
+ result
25
+ end
26
+ end
27
+
28
+ config.after(:suite) do
29
+ records = records_by_line[#{line}]
30
+ records&.any? || exit!(#{FACTORY_UNUSED_CODE})
31
+ unless "#{variant}".include?('stub') # factory_bot stub stubs persisted? as true
32
+ records.any? { |r| r.respond_to?(:persisted?) && r.persisted? } &&
33
+ exit!(#{FACTORY_PERSISTED_LATER_CODE})
34
+ end
35
+ end
36
+ end
37
+ RUBY
38
+ end
39
+ end
@@ -2,46 +2,30 @@ module FactorySloth
2
2
  module FileProcessor
3
3
  extend self
4
4
 
5
- def call(files:, forced_files: [], dry_run: false)
5
+ def call(files:, forced_files: [])
6
6
  files.each_with_object({}) do |path, acc|
7
- puts "Processing #{path} ..."
8
-
9
- if DoneTracker.done?(path) && !forced_files.include?(path)
10
- puts "πŸ”΅ Skipped (marked as done in #{DoneTracker.file})", ''
7
+ if DoneTracker.done?(path) &&
8
+ !(FactorySloth.force || forced_files.include?(path))
9
+ puts "πŸ”΅ #{path}: skipped (marked as done in #{DoneTracker.file})", ''
11
10
  next
12
11
  end
13
12
 
14
- result = process(path, dry_run: dry_run)
15
- acc[path] = { ok: result.ok?, changed_create_calls: result.changed_create_calls }
13
+ result = process(path)
14
+ acc[path] = { ok: result.ok?, change_count: result.change_count }
16
15
  DoneTracker.mark_as_done(path)
17
16
  end
18
17
  end
19
18
 
20
19
  private
21
20
 
22
- def process(path, dry_run:)
21
+ def process(path)
23
22
  code = File.read(path)
24
23
  result = CodeMod.call(path, code)
25
- unless dry_run
24
+ unless FactorySloth.dry_run
26
25
  File.write(path, result.patched_code) if result.changed?
27
26
  end
28
- puts result_message(result, dry_run), ''
27
+ puts result.message, ''
29
28
  result
30
29
  end
31
-
32
- def result_message(result, dry_run)
33
- stats = "#{result.create_count} create calls found, "\
34
- "#{result.change_count} #{dry_run ? 'replaceable' : 'replaced'}"
35
-
36
- return "πŸ”΄ #{stats} (conflict)" unless result.ok?
37
-
38
- if result.create_count == 0
39
- "βšͺ️ #{stats}"
40
- elsif result.change_count == 0
41
- "🟑 #{stats}"
42
- else
43
- "🟒 #{stats}"
44
- end
45
- end
46
30
  end
47
31
  end
@@ -8,8 +8,14 @@ module FactorySloth::SpecRunner
8
8
  File.write(path, spec_code)
9
9
  path_arg = [path, line].compact.map(&:to_s).join(':')
10
10
  command = "bundle exec rspec #{path_arg} --fail-fast --order defined 2>&1"
11
- _output, status = Open3.capture2(command)
12
- status
11
+ output, process_status = Open3.capture2(command)
12
+ Result.new(output: output, process_status: process_status)
13
+ end
14
+
15
+ Result = Struct.new(:output, :process_status, keyword_init: true) do
16
+ require 'forwardable'
17
+ extend Forwardable
18
+ def_delegators :process_status, :exitstatus, :success?
13
19
  end
14
20
 
15
21
  def self.tmpdir
@@ -1,3 +1,3 @@
1
1
  module FactorySloth
2
- VERSION = '1.2.2'
2
+ VERSION = '1.3.0'
3
3
  end
data/lib/factory_sloth.rb CHANGED
@@ -1,10 +1,14 @@
1
- module FactorySloth; end
1
+ module FactorySloth
2
+ singleton_class.attr_accessor :dry_run, :force, :lint, :verbose
3
+ end
2
4
 
3
5
  require_relative 'factory_sloth/cli'
4
6
  require_relative 'factory_sloth/code_mod'
7
+ require_relative 'factory_sloth/color'
5
8
  require_relative 'factory_sloth/create_call'
6
9
  require_relative 'factory_sloth/create_call_finder'
7
10
  require_relative 'factory_sloth/done_tracker'
11
+ require_relative 'factory_sloth/execution_check'
8
12
  require_relative 'factory_sloth/file_processor'
9
13
  require_relative 'factory_sloth/spec_picker'
10
14
  require_relative 'factory_sloth/spec_runner'
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.2
4
+ version: 1.3.0
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-19 00:00:00.000000000 Z
11
+ date: 2023-05-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -28,9 +28,11 @@ 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/color.rb
31
32
  - lib/factory_sloth/create_call.rb
32
33
  - lib/factory_sloth/create_call_finder.rb
33
34
  - lib/factory_sloth/done_tracker.rb
35
+ - lib/factory_sloth/execution_check.rb
34
36
  - lib/factory_sloth/file_processor.rb
35
37
  - lib/factory_sloth/spec_picker.rb
36
38
  - lib/factory_sloth/spec_runner.rb