ai_refactor 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -2
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +5 -1
  5. data/README.md +68 -24
  6. data/Rakefile +1 -1
  7. data/ai_refactor.gemspec +1 -0
  8. data/exe/ai_refactor +78 -44
  9. data/lib/ai_refactor/cli.rb +86 -0
  10. data/lib/ai_refactor/context.rb +33 -0
  11. data/lib/ai_refactor/file_processor.rb +34 -17
  12. data/lib/ai_refactor/prompt.rb +84 -0
  13. data/lib/ai_refactor/prompts/diff.md +17 -0
  14. data/lib/ai_refactor/prompts/input.md +1 -0
  15. data/lib/ai_refactor/refactors/base_refactor.rb +176 -0
  16. data/lib/ai_refactor/refactors/generic.rb +6 -80
  17. data/lib/ai_refactor/refactors/minitest/write_test_for_class.md +11 -0
  18. data/lib/ai_refactor/refactors/minitest/write_test_for_class.rb +51 -0
  19. data/lib/ai_refactor/refactors/project/write_changelog_from_history.md +35 -0
  20. data/lib/ai_refactor/refactors/project/write_changelog_from_history.rb +50 -0
  21. data/lib/ai_refactor/refactors/{prompts/rspec_to_minitest_rails.md → rails/minitest/rspec_to_minitest.md} +40 -1
  22. data/lib/ai_refactor/refactors/rails/minitest/rspec_to_minitest.rb +77 -0
  23. data/lib/ai_refactor/refactors/rspec/minitest_to_rspec.rb +13 -0
  24. data/lib/ai_refactor/refactors.rb +13 -5
  25. data/lib/ai_refactor/{refactors/tests → test_runners}/minitest_runner.rb +1 -1
  26. data/lib/ai_refactor/{refactors/tests → test_runners}/rspec_runner.rb +1 -1
  27. data/lib/ai_refactor/{refactors/tests → test_runners}/test_run_diff_report.rb +1 -1
  28. data/lib/ai_refactor/{refactors/tests → test_runners}/test_run_result.rb +1 -1
  29. data/lib/ai_refactor/version.rb +1 -1
  30. data/lib/ai_refactor.rb +13 -8
  31. metadata +34 -11
  32. data/lib/ai_refactor/base_refactor.rb +0 -70
  33. data/lib/ai_refactor/refactors/minitest_to_rspec.rb +0 -11
  34. data/lib/ai_refactor/refactors/rspec_to_minitest_rails.rb +0 -103
  35. /data/lib/ai_refactor/refactors/{prompts → rspec}/minitest_to_rspec.md +0 -0
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ class Prompt
5
+ INPUT_FILE_PATH_MARKER = "__{{input_file_path}}__"
6
+ OUTPUT_FILE_PATH_MARKER = "__{{output_file_path}}__"
7
+ HEADER_MARKER = "__{{prompt_header}}__"
8
+ FOOTER_MARKER = "__{{prompt_footer}}__"
9
+ CONTEXT_MARKER = "__{{context}}__"
10
+ CONTENT_MARKER = "__{{content}}__"
11
+
12
+ attr_reader :input_file_path, :prompt_file_path
13
+
14
+ def initialize(options:, logger:, context: nil, input_content: nil, input_path: nil, output_file_path: nil, prompt_file_path: nil, prompt_header: nil, prompt_footer: nil)
15
+ @input_content = input_content
16
+ @input_file_path = input_path
17
+ @output_file_path = output_file_path
18
+ @prompt_file_path = prompt_file_path
19
+ @logger = logger
20
+ @header = prompt_header
21
+ @footer = prompt_footer
22
+ @diff = options[:diff]
23
+ @context = context
24
+ end
25
+
26
+ def chat_messages
27
+ [
28
+ {role: "system", content: system_prompt},
29
+ {role: "user", content: user_prompt}
30
+ ]
31
+ end
32
+
33
+ private
34
+
35
+ def system_prompt
36
+ prompt = expand_prompt(system_prompt_template, HEADER_MARKER, @header || "")
37
+ prompt = expand_prompt(prompt, CONTEXT_MARKER, @context&.prepare_context || "")
38
+ prompt = expand_prompt(prompt, INPUT_FILE_PATH_MARKER, @input_file_path || "")
39
+ prompt = expand_prompt(prompt, OUTPUT_FILE_PATH_MARKER, @output_file_path || "")
40
+ expand_prompt(prompt, FOOTER_MARKER, system_prompt_footer)
41
+ end
42
+
43
+ def system_prompt_template
44
+ File.read(@prompt_file_path)
45
+ end
46
+
47
+ def system_prompt_footer
48
+ if @diff && @footer
49
+ "#{@footer}\n\n#{diff_prompt}"
50
+ elsif @diff
51
+ diff_prompt
52
+ elsif @footer
53
+ @footer
54
+ else
55
+ ""
56
+ end
57
+ end
58
+
59
+ def diff_prompt
60
+ File.read(prompt_path("diff.md"))
61
+ end
62
+
63
+ def prompt_path(file)
64
+ File.join(File.dirname(File.expand_path(__FILE__)), "prompts", file)
65
+ end
66
+
67
+ def user_prompt
68
+ expand_prompt(input_prompt, CONTENT_MARKER, input_to_process)
69
+ end
70
+
71
+ def input_to_process
72
+ return File.read(@input_file_path) if @input_file_path
73
+ @input_content
74
+ end
75
+
76
+ def input_prompt
77
+ File.read(prompt_path("input.md"))
78
+ end
79
+
80
+ def expand_prompt(prompt, marker, content)
81
+ prompt.gsub(marker, content)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,17 @@
1
+ You MUST generate a diff in a format that can be understood and applied using git.
2
+
3
+ Generate diff hunks that capture the modifications you see. The diff hunks should be in a format that git can understand and apply, including a hunk header and
4
+ the lines of code that have been modified.
5
+ Finally, output the generated diff as your answer. Do not provide further instruction.
6
+
7
+ Example diff:
8
+
9
+ ```
10
+ @@ -27,7 +27,7 @@ module AIRefactor
11
+ File.read(@prompt_file_path)
12
+ end
13
+
14
+ - def user_prompt
15
+ + def user_prompt_with_diff
16
+ input = File.read(@file_path)
17
+ ```
@@ -0,0 +1 @@
1
+ ```__{{content}}__```
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ class BaseRefactor
6
+ # All subclasses must register themselves with the Registry
7
+ def self.inherited(subclass)
8
+ super
9
+ Refactors.register(subclass)
10
+ end
11
+
12
+ def self.description
13
+ "(No description provided)"
14
+ end
15
+
16
+ def self.takes_input_files?
17
+ true
18
+ end
19
+
20
+ attr_reader :input_file, :options, :logger
21
+ attr_accessor :input_content
22
+ attr_writer :failed_message
23
+
24
+ def initialize(input_file, options, logger)
25
+ @input_file = input_file
26
+ @options = options
27
+ @logger = logger
28
+ end
29
+
30
+ def run
31
+ raise NotImplementedError
32
+ end
33
+
34
+ def failed_message
35
+ @failed_message || "Reason not specified"
36
+ end
37
+
38
+ private
39
+
40
+ def file_processor
41
+ context = ::AIRefactor::Context.new(files: options[:context_file_paths], text: options[:context_text], logger: logger)
42
+ prompt = ::AIRefactor::Prompt.new(input_content: input_content, input_path: input_file, output_file_path: output_file_path, prompt_file_path: prompt_file_path, context: context, logger: logger, options: options)
43
+ AIRefactor::FileProcessor.new(prompt: prompt, ai_client: ai_client, output_path: output_file_path, logger: logger, options: options)
44
+ end
45
+
46
+ def process!(strip_ticks: true)
47
+ processor = file_processor
48
+
49
+ if processor.output_exists?
50
+ return false unless overwrite_existing_output?(output_file_path)
51
+ end
52
+
53
+ logger.verbose "Processing #{input_file}..."
54
+
55
+ begin
56
+ output_content, finished_reason, usage = processor.process! do |content|
57
+ if block_given?
58
+ yield content
59
+ elsif strip_ticks
60
+ content.gsub("```ruby", "").gsub("```", "")
61
+ else
62
+ content
63
+ end
64
+ end
65
+
66
+ logger.verbose "AI finished, with reason '#{finished_reason}'..."
67
+ logger.verbose "Used tokens: #{usage["total_tokens"]}".colorize(:light_black) if usage
68
+ if finished_reason == "length"
69
+ logger.warn "Translation may contain an incomplete output as the max token length was reached. You can try using the '--continue' option next time to increase the length of generated output."
70
+ end
71
+
72
+ if !output_content || output_content.length == 0
73
+ logger.warn "Skipping #{input_file}, no translated output..."
74
+ logger.error "Failed to translate #{input_file}, finished reason #{finished_reason}"
75
+ self.failed_message = "AI conversion failed, no output was generated"
76
+ raise NoOutputError, "No output"
77
+ end
78
+
79
+ output_content
80
+ rescue => e
81
+ logger.error "Request to AI failed: #{e.message}"
82
+ logger.warn "Skipping #{input_file}..."
83
+ self.failed_message = "Request to OpenAI failed"
84
+ raise e
85
+ end
86
+ end
87
+
88
+ def overwrite_existing_output?(output_path)
89
+ overwrite = options && options[:overwrite]&.downcase
90
+ answer = if ["y", "n"].include? overwrite
91
+ overwrite
92
+ else
93
+ logger.info "Do you wish to overwrite #{output_path}? (y/n)"
94
+ $stdin.gets.chomp.downcase
95
+ end
96
+ if answer == "y"
97
+ logger.verbose "Overwriting #{output_path}..."
98
+ return true
99
+ end
100
+ logger.warn "Skipping #{input_file}..."
101
+ self.failed_message = "Skipped as output file already exists"
102
+ false
103
+ end
104
+
105
+ def prompt_file_path
106
+ file = if options && options[:prompt_file_path]&.length&.positive?
107
+ options[:prompt_file_path]
108
+ else
109
+ location = Module.const_source_location(::AIRefactor::Refactors::BaseRefactor.name)
110
+ File.join(File.dirname(location.first), "#{refactor_name}.md")
111
+ end
112
+ file.tap do |prompt|
113
+ raise "No prompt file '#{prompt}' found for #{refactor_name}" unless File.exist?(prompt)
114
+ end
115
+ end
116
+
117
+ def output_file_path
118
+ @output_file_path ||= determine_output_file_path
119
+ end
120
+
121
+ def determine_output_file_path
122
+ return output_file_path_from_template if output_template_path
123
+
124
+ path = options[:output_file_path]
125
+ return default_output_path unless path
126
+
127
+ if path == true
128
+ input_file
129
+ else
130
+ path
131
+ end
132
+ end
133
+
134
+ def default_output_path
135
+ nil
136
+ end
137
+
138
+ def output_template_path
139
+ options[:output_template_path]
140
+ end
141
+
142
+ def output_file_path_from_template
143
+ path = output_template_path.gsub("[FILE]", File.basename(input_file))
144
+ .gsub("[NAME]", File.basename(input_file, ".*"))
145
+ .gsub("[DIR]", File.dirname(input_file))
146
+ .gsub("[REFACTOR]", self.class.refactor_name)
147
+ .gsub("[EXT]", File.extname(input_file))
148
+ raise "Output template could not be used" unless path.length.positive?
149
+ path
150
+ end
151
+
152
+ def ai_client
153
+ @ai_client ||= OpenAI::Client.new
154
+ end
155
+
156
+ def refactor_name
157
+ self.class.refactor_name
158
+ end
159
+
160
+ class << self
161
+ def command_line_options
162
+ []
163
+ end
164
+
165
+ def refactor_name
166
+ name.gsub("AIRefactor::Refactors::", "")
167
+ .gsub(/::/, "/")
168
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
169
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
170
+ .tr("-", "_")
171
+ .downcase
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -7,74 +7,23 @@ module AIRefactor
7
7
  logger.verbose "Generic refactor to #{input_file}... (using user supplied prompt #{prompt_file_path})"
8
8
  logger.verbose "Write output to #{output_file_path}..." if output_file_path
9
9
 
10
- processor = AIRefactor::FileProcessor.new(
11
- input_path: input_file,
12
- prompt_file_path: prompt_file_path,
13
- ai_client: ai_client,
14
- logger: logger,
15
- output_path: output_file_path
16
- )
17
-
18
- if processor.output_exists?
19
- return false unless can_overwrite_output_file?(output_file_path)
20
- end
21
-
22
- logger.verbose "Converting #{input_file}..."
23
-
24
10
  begin
25
- output_content, finished_reason, usage = processor.process!(options)
11
+ output_content = process!(strip_ticks: false)
26
12
  rescue => e
27
- logger.error "Request to OpenAI failed: #{e.message}"
28
- logger.warn "Skipping #{input_file}..."
29
- self.failed_message = "Request to OpenAI failed"
13
+ logger.error "Failed to process #{input_file}: #{e.message}"
30
14
  return false
31
15
  end
32
16
 
33
- logger.verbose "OpenAI finished, with reason '#{finished_reason}'..."
34
- logger.verbose "Used tokens: #{usage["total_tokens"]}".colorize(:light_black) if usage
35
-
36
- if finished_reason == "length"
37
- logger.warn "Translation may contain an incomplete output as the max token length was reached. You can try using the '--continue' option next time to increase the length of generated output."
38
- end
39
-
40
- if !output_content || output_content.length == 0
41
- logger.warn "Skipping #{input_file}, no translated output..."
42
- logger.error "Failed to translate #{input_file}, finished reason #{finished_reason}"
43
- self.failed_message = "AI conversion failed, no output was generated"
44
- return false
45
- end
17
+ return false unless output_content
46
18
 
47
19
  output_file_path ? true : output_content
48
20
  end
49
21
 
50
- private
51
-
52
- def output_file_path
53
- return output_file_path_from_template if output_template_path
54
-
55
- path = options[:output_file_path]
56
- return unless path
57
-
58
- if path == true
59
- input_file
60
- else
61
- path
62
- end
63
- end
64
-
65
- def output_template_path
66
- options[:output_template_path]
22
+ def self.description
23
+ "Generic refactor using user supplied prompt"
67
24
  end
68
25
 
69
- def output_file_path_from_template
70
- path = output_template_path.gsub("[FILE]", File.basename(input_file))
71
- .gsub("[NAME]", File.basename(input_file, ".*"))
72
- .gsub("[DIR]", File.dirname(input_file))
73
- .gsub("[REFACTOR]", self.class.refactor_name)
74
- .gsub("[EXT]", File.extname(input_file))
75
- raise "Output template could not be used" unless path.length.positive?
76
- path
77
- end
26
+ private
78
27
 
79
28
  def prompt_file_path
80
29
  specified_prompt_path = options[:prompt_file_path]
@@ -89,29 +38,6 @@ module AIRefactor
89
38
  end
90
39
  exit 1
91
40
  end
92
-
93
- class << self
94
- def prompt_file_path
95
- raise "Generic refactor requires prompt file to be user specified."
96
- end
97
-
98
- def command_line_options
99
- [
100
- {
101
- key: :output_file_path,
102
- long: "--output [FILE]",
103
- type: String,
104
- help: "Write output to file instead of stdout. If no path provided will overwrite input file (will prompt to overwrite existing files)"
105
- },
106
- {
107
- key: :output_template_path,
108
- long: "--output-template TEMPLATE",
109
- type: String,
110
- help: "Write outputs to files instead of stdout. The template is used to create the output name, where the it can have substitutions, '[FILE]', '[NAME]', '[DIR]', '[REFACTOR]' & '[EXT]'. Eg `[DIR]/[NAME]_[REFACTOR][EXT]` (will prompt to overwrite existing files)"
111
- }
112
- ]
113
- end
114
- end
115
41
  end
116
42
  end
117
43
  end
@@ -0,0 +1,11 @@
1
+ You are an expert Ruby senior software developer. Write a unit test for the class show below, using minitest and minitest/mock if necessary.
2
+ Test 100% of the code.
3
+
4
+ The path to the file to test is: __{{input_file_path}}__
5
+ The output file path is: __{{output_file_path}}__
6
+
7
+ Only show me the test file code. Do NOT provide any other description of your work. Always enclose the output code in triple backticks (```).
8
+
9
+ __{{context}}__
10
+
11
+ The class to test is:
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ module Minitest
6
+ class WriteTestForClass < BaseRefactor
7
+ def run
8
+ logger.verbose "'Write minitest test' refactor for #{input_file}..."
9
+ logger.verbose "Write output to #{output_file_path}..." if output_file_path
10
+
11
+ begin
12
+ output_content = process!
13
+ rescue => e
14
+ logger.error "Failed to process #{input_file}: #{e.message}"
15
+ return false
16
+ end
17
+
18
+ return false unless output_file_path
19
+
20
+ logger.verbose "Generated #{output_file_path} from #{input_file} ..." if output_content
21
+
22
+ minitest_runner = AIRefactor::TestRunners::MinitestRunner.new(output_file_path, command_template: "bundle exec ruby __FILE__")
23
+
24
+ logger.verbose "Run generated test file #{output_file_path} (#{minitest_runner.command})..."
25
+ test_run = minitest_runner.run
26
+
27
+ if test_run.failed?
28
+ logger.warn "#{input_file} was translated to #{output_file_path} but the resulting test is failing..."
29
+ logger.error "Failed to run test, exited with status #{test_run.exitstatus}. Stdout: #{test_run.stdout}\n\nStderr: #{test_run.stderr}\n\n"
30
+ logger.error "New test failed!", bold: true
31
+ self.failed_message = "Generated test file failed to run correctly"
32
+ return false
33
+ end
34
+
35
+ logger.verbose "\nNew test file ran and returned the following results:"
36
+ logger.verbose ">> Runs: #{test_run.example_count}, Failures: #{test_run.failure_count}, Skips: #{test_run.pending_count}\n"
37
+
38
+ output_file_path ? true : output_content
39
+ end
40
+
41
+ def self.description
42
+ "Write a minitest test for a class"
43
+ end
44
+
45
+ def default_output_path
46
+ File.join("test", input_file.gsub(/\.rb$/, "_test.rb"))
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,35 @@
1
+ You are a senior Ruby software developer. You are diligent about writing clear changelogs for your users.
2
+
3
+ You have a git history of changes to your project. You want to write a changelog from this history.
4
+
5
+ The format of the changelog you will create entries for is from keepachangelog.com version 1.0.0.
6
+
7
+ Here is an example of a set of changelog entries:
8
+
9
+ ```
10
+ ## [0.2.0] - 2015-10-06
11
+
12
+ ### Changed
13
+
14
+ - Remove exclusionary mentions of "open source" since this project can
15
+ benefit both "open" and "closed" source projects equally.
16
+
17
+ ## [0.1.0] - 2015-10-06
18
+
19
+ ### Added
20
+
21
+ - Answer "Should you ever rewrite a change log?".
22
+
23
+ ### Changed
24
+
25
+ - Improve argument against commit logs.
26
+ - Start following [SemVer](https://semver.org) properly.
27
+
28
+ ### Fixed
29
+
30
+ - Fix typos in recent CHANGELOG entries.
31
+ ```
32
+
33
+ If the history contains any mention of changes of version (such as "Bump version to 1.0.0"), then you should use that version number in a new changelog entry.
34
+
35
+ Now write changelog entries from the following git history.
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ module Project
6
+ class WriteChangelogFromHistory < BaseRefactor
7
+ def self.description
8
+ "Write changelog entries from the git history"
9
+ end
10
+
11
+ def self.takes_input_files?
12
+ false
13
+ end
14
+
15
+ def run
16
+ logger.verbose "Creating changelog entries for project from #{options[:git_commit_count] || 3} commits..."
17
+ logger.verbose "Write output to #{output_file_path}..." if output_file_path
18
+
19
+ self.input_content = `git log -#{options[:git_commit_count] || 3} --pretty=format:"%ci %d %s"`.split("\n").map { |line| "- #{line}" }.join("\n")
20
+ logger.debug "\nInput messages: \n#{input_content}\n\n"
21
+ begin
22
+ output_content = process!(strip_ticks: false)
23
+ rescue => e
24
+ logger.error "Failed to process: #{e.message}"
25
+ return false
26
+ end
27
+
28
+ return false unless output_content
29
+
30
+ output_file_path ? true : output_content
31
+ end
32
+
33
+ def default_output_path
34
+ nil
35
+ end
36
+
37
+ def self.command_line_options
38
+ [
39
+ {
40
+ key: :git_commit_count,
41
+ long: "--commits N",
42
+ type: Integer,
43
+ help: "The number of commits to analyse when creating the changelog entries (defaults to 3)"
44
+ }
45
+ ]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,8 +1,8 @@
1
+ You are an expert software developer.
1
2
  You convert RSpec tests to ActiveSupport::TestCase tests for Ruby on Rails.
2
3
  ActiveSupport::TestCase uses MiniTest under the hood.
3
4
  Remember that MiniTest does not support `context` blocks, instead these should be removed and the context
4
5
  specified in them should be moved directly into the relevant tests.
5
- Always enclose the output code in triple backticks (```).
6
6
 
7
7
  Here are some examples to use as a guide:
8
8
 
@@ -42,6 +42,10 @@ subject(:model) { create(:order_state) }
42
42
  context "when rejected" do
43
43
  before { model.rejected_at = 1.day.ago }
44
44
 
45
+ it "should be not valid" do
46
+ expect(model).not_to be_valid
47
+ end
48
+
45
49
  context "with reason and message" do
46
50
  before do
47
51
  model.rejected_message = reason
@@ -68,6 +72,11 @@ setup do
68
72
  @reason = "my reason"
69
73
  end
70
74
 
75
+ test "when rejected, model should be not valid" do
76
+ @model.rejected_at = 1.day.ago
77
+ refute @model.valid?
78
+ end
79
+
71
80
  test "when rejected, with reason and message, model should be valid" do
72
81
  @model.rejected_at = 1.day.ago
73
82
  @model.rejected_message = @reason
@@ -231,3 +240,33 @@ test "stubs any instance" do
231
240
  end
232
241
  end
233
242
  ```
243
+
244
+ Example 9) RSpec:
245
+ ```
246
+ assert_association @model, :message_thread, :belongs_to
247
+ ```
248
+
249
+ Result 9) minitest:
250
+
251
+ ```
252
+ assert_instance_of MessageThread, @model.message_thread
253
+ ```
254
+
255
+ Example 10) RSpec:
256
+ ```
257
+ assert_association @model, :message_thread, :belongs_to, optional: true
258
+ ```
259
+
260
+ Result 10) minitest:
261
+ ```
262
+ assoc = @model.reflect_on_association(:message_thread)
263
+ refute assoc.nil?, "no association :message_thread"
264
+ assert_equal :belongs_to, assoc.macro
265
+ assert assoc.options[:optional]
266
+ ```
267
+
268
+ __{{context}}__
269
+
270
+ Only output the refactored class. Do NOT provide any other description of your work. Always enclose the output code in triple backticks (```).
271
+
272
+ Convert this Rspec file to minitest:
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ module Rails
6
+ module Minitest
7
+ class RspecToMinitest < BaseRefactor
8
+ def run
9
+ spec_runner = AIRefactor::TestRunners::RSpecRunner.new(input_file)
10
+ logger.verbose "Run spec #{input_file}... (#{spec_runner.command})"
11
+
12
+ spec_run = spec_runner.run
13
+
14
+ if spec_run.failed?
15
+ logger.warn "Skipping #{input_file}..."
16
+ logger.error "Failed to run #{input_file}, exited with status #{spec_run.exitstatus}. Stdout: #{spec_run.stdout}\n\nStderr: #{spec_run.stderr}\n\n"
17
+ self.failed_message = "Failed to run RSpec file, has errors"
18
+ return false
19
+ end
20
+
21
+ logger.debug "\nOriginal test run results:"
22
+ logger.debug ">> Examples: #{spec_run.example_count}, Failures: #{spec_run.failure_count}, Pendings: #{spec_run.pending_count}\n"
23
+
24
+ begin
25
+ result = process!
26
+ rescue AIRefactor::NoOutputError => _e
27
+ return false
28
+ rescue => e
29
+ logger.error "Failed to convert #{input_file} to Minitest, error: #{e.message}"
30
+ return false
31
+ end
32
+
33
+ logger.verbose "Converted #{input_file} to #{output_file_path}..." if result
34
+
35
+ minitest_runner = AIRefactor::TestRunners::MinitestRunner.new(output_file_path)
36
+
37
+ logger.verbose "Run generated test file #{output_file_path} (#{minitest_runner.command})..."
38
+ test_run = minitest_runner.run
39
+
40
+ if test_run.failed?
41
+ logger.warn "Skipping #{input_file}..."
42
+ logger.error "Failed to run translated #{output_file_path}, exited with status #{test_run.exitstatus}. Stdout: #{test_run.stdout}\n\nStderr: #{test_run.stderr}\n\n"
43
+ logger.error "Conversion failed!", bold: true
44
+ self.failed_message = "Generated test file failed to run correctly"
45
+ return false
46
+ end
47
+
48
+ logger.debug "\nTranslated test file results:"
49
+ logger.debug ">> Runs: #{test_run.example_count}, Failures: #{test_run.failure_count}, Skips: #{test_run.pending_count}\n"
50
+
51
+ report = AIRefactor::TestRunners::TestRunDiffReport.new(spec_run, test_run)
52
+
53
+ if report.no_differences?
54
+ logger.verbose "Done converting #{input_file} to #{output_file_path}..."
55
+ logger.success "\nNo differences found! Conversion worked!"
56
+ true
57
+ else
58
+ logger.warn report.diff.colorize(:yellow)
59
+ logger.verbose "Done converting #{input_file} to #{output_file_path}..."
60
+ logger.error "\nDifferences found! Conversion failed!", bold: true
61
+ self.failed_message = "Generated test file run output did not match original RSpec spec run output"
62
+ false
63
+ end
64
+ end
65
+
66
+ def self.description
67
+ "Convert RSpec file to Minitest (for Rails apps)"
68
+ end
69
+
70
+ def default_output_path
71
+ input_file.gsub("_spec.rb", "_test.rb").gsub("spec/", "test/")
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end