ai_refactor 0.3.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -2
- data/Gemfile +9 -0
- data/Gemfile.lock +169 -1
- data/README.md +169 -43
- data/Rakefile +1 -1
- data/ai_refactor.gemspec +1 -0
- data/examples/.gitignore +1 -0
- data/examples/ex1_convert_a_rspec_test_to_minitest.yml +7 -0
- data/examples/ex1_input_spec.rb +32 -0
- data/examples/rails_helper.rb +21 -0
- data/examples/test_helper.rb +14 -0
- data/exe/ai_refactor +139 -52
- data/lib/ai_refactor/cli.rb +138 -0
- data/lib/ai_refactor/command_file_parser.rb +27 -0
- data/lib/ai_refactor/context.rb +33 -0
- data/lib/ai_refactor/file_processor.rb +34 -17
- data/lib/ai_refactor/prompt.rb +84 -0
- data/lib/ai_refactor/prompts/diff.md +17 -0
- data/lib/ai_refactor/prompts/input.md +1 -0
- data/lib/ai_refactor/refactors/base_refactor.rb +183 -0
- data/lib/ai_refactor/refactors/custom.rb +43 -0
- data/lib/ai_refactor/refactors/minitest/write_test_for_class.md +15 -0
- data/lib/ai_refactor/refactors/minitest/write_test_for_class.rb +51 -0
- data/lib/ai_refactor/refactors/project/write_changelog_from_history.md +35 -0
- data/lib/ai_refactor/refactors/project/write_changelog_from_history.rb +50 -0
- data/lib/ai_refactor/refactors/{prompts/rspec_to_minitest_rails.md → rails/minitest/rspec_to_minitest.md} +40 -1
- data/lib/ai_refactor/refactors/rails/minitest/rspec_to_minitest.rb +77 -0
- data/lib/ai_refactor/refactors/rspec/minitest_to_rspec.rb +13 -0
- data/lib/ai_refactor/refactors/ruby/refactor_ruby.md +10 -0
- data/lib/ai_refactor/refactors/ruby/refactor_ruby.rb +29 -0
- data/lib/ai_refactor/refactors/ruby/write_ruby.md +7 -0
- data/lib/ai_refactor/refactors/ruby/write_ruby.rb +33 -0
- data/lib/ai_refactor/refactors.rb +13 -5
- data/lib/ai_refactor/run_configuration.rb +115 -0
- data/lib/ai_refactor/{refactors/tests → test_runners}/minitest_runner.rb +2 -2
- data/lib/ai_refactor/{refactors/tests → test_runners}/rspec_runner.rb +1 -1
- data/lib/ai_refactor/{refactors/tests → test_runners}/test_run_diff_report.rb +1 -1
- data/lib/ai_refactor/{refactors/tests → test_runners}/test_run_result.rb +1 -1
- data/lib/ai_refactor/version.rb +1 -1
- data/lib/ai_refactor.rb +13 -8
- metadata +47 -13
- data/lib/ai_refactor/base_refactor.rb +0 -66
- data/lib/ai_refactor/refactors/generic.rb +0 -113
- data/lib/ai_refactor/refactors/minitest_to_rspec.rb +0 -11
- data/lib/ai_refactor/refactors/rspec_to_minitest_rails.rb +0 -103
- /data/lib/ai_refactor/refactors/{prompts → rspec}/minitest_to_rspec.md +0 -0
@@ -0,0 +1,183 @@
|
|
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: prompt_input, 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
|
+
puts e.backtrace
|
83
|
+
logger.warn "Skipping #{input_file}..."
|
84
|
+
self.failed_message = "Request to OpenAI failed"
|
85
|
+
raise e
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def overwrite_existing_output?(output_path)
|
90
|
+
overwrite = options && options[:overwrite]&.downcase
|
91
|
+
answer = if ["y", "n"].include? overwrite
|
92
|
+
overwrite
|
93
|
+
else
|
94
|
+
logger.info "Do you wish to overwrite #{output_path}? (y/n)"
|
95
|
+
$stdin.gets.chomp.downcase
|
96
|
+
end
|
97
|
+
if answer == "y"
|
98
|
+
logger.verbose "Overwriting #{output_path}..."
|
99
|
+
return true
|
100
|
+
end
|
101
|
+
logger.warn "Skipping #{input_file}..."
|
102
|
+
self.failed_message = "Skipped as output file already exists"
|
103
|
+
false
|
104
|
+
end
|
105
|
+
|
106
|
+
def prompt_input
|
107
|
+
if options && options[:prompt]&.length&.positive?
|
108
|
+
return options[:prompt]
|
109
|
+
end
|
110
|
+
|
111
|
+
file = if options && options[:prompt_file_path]&.length&.positive?
|
112
|
+
options[:prompt_file_path]
|
113
|
+
else
|
114
|
+
location = Module.const_source_location(::AIRefactor::Refactors::BaseRefactor.name)
|
115
|
+
File.join(File.dirname(location.first), "#{refactor_name}.md")
|
116
|
+
end
|
117
|
+
file.tap do |prompt|
|
118
|
+
raise "No prompt file '#{prompt}' found for #{refactor_name}" unless File.exist?(prompt)
|
119
|
+
end
|
120
|
+
|
121
|
+
File.read(file)
|
122
|
+
end
|
123
|
+
|
124
|
+
def output_file_path
|
125
|
+
@output_file_path ||= determine_output_file_path
|
126
|
+
end
|
127
|
+
|
128
|
+
def determine_output_file_path
|
129
|
+
return output_file_path_from_template if output_template_path
|
130
|
+
|
131
|
+
path = options[:output_file_path]
|
132
|
+
return default_output_path unless path
|
133
|
+
|
134
|
+
if path == true
|
135
|
+
input_file
|
136
|
+
else
|
137
|
+
path
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def default_output_path
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
|
145
|
+
def output_template_path
|
146
|
+
options[:output_template_path]
|
147
|
+
end
|
148
|
+
|
149
|
+
def output_file_path_from_template
|
150
|
+
path = output_template_path.gsub("[FILE]", File.basename(input_file))
|
151
|
+
.gsub("[NAME]", File.basename(input_file, ".*"))
|
152
|
+
.gsub("[DIR]", File.dirname(input_file))
|
153
|
+
.gsub("[REFACTOR]", self.class.refactor_name)
|
154
|
+
.gsub("[EXT]", File.extname(input_file))
|
155
|
+
raise "Output template could not be used" unless path.length.positive?
|
156
|
+
path
|
157
|
+
end
|
158
|
+
|
159
|
+
def ai_client
|
160
|
+
@ai_client ||= OpenAI::Client.new
|
161
|
+
end
|
162
|
+
|
163
|
+
def refactor_name
|
164
|
+
self.class.refactor_name
|
165
|
+
end
|
166
|
+
|
167
|
+
class << self
|
168
|
+
def command_line_options
|
169
|
+
[]
|
170
|
+
end
|
171
|
+
|
172
|
+
def refactor_name
|
173
|
+
name.gsub("AIRefactor::Refactors::", "")
|
174
|
+
.gsub(/::/, "/")
|
175
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
176
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
177
|
+
.tr("-", "_")
|
178
|
+
.downcase
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
module Refactors
|
5
|
+
class Custom < BaseRefactor
|
6
|
+
def run
|
7
|
+
logger.verbose "Custom refactor to #{input_file}... (using user supplied prompt #{prompt_file_path})"
|
8
|
+
logger.verbose "Write output to #{output_file_path}..." if output_file_path
|
9
|
+
|
10
|
+
begin
|
11
|
+
output_content = process!(strip_ticks: false)
|
12
|
+
rescue => e
|
13
|
+
logger.error "Failed to process #{input_file}: #{e.message}"
|
14
|
+
return false
|
15
|
+
end
|
16
|
+
|
17
|
+
return false unless output_content
|
18
|
+
|
19
|
+
output_file_path ? true : output_content
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.description
|
23
|
+
"Generic refactor using user supplied prompt"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def prompt_file_path
|
29
|
+
specified_prompt_path = options[:prompt_file_path]
|
30
|
+
if specified_prompt_path&.length&.positive?
|
31
|
+
if File.exist?(specified_prompt_path)
|
32
|
+
return specified_prompt_path
|
33
|
+
else
|
34
|
+
logger.error "No prompt file '#{specified_prompt_path}' found"
|
35
|
+
end
|
36
|
+
else
|
37
|
+
logger.error "No prompt file was specified!"
|
38
|
+
end
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,15 @@
|
|
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
|
+
__{{prompt_header}}__
|
8
|
+
|
9
|
+
Only show me the test file code. Do NOT provide any other description of your work. Always enclose the output code in triple backticks (```).
|
10
|
+
|
11
|
+
__{{context}}__
|
12
|
+
|
13
|
+
__{{prompt_footer}}__
|
14
|
+
|
15
|
+
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, command_template: options.rspec_run_command)
|
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, command_template: options.minitest_run_command)
|
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
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
module Refactors
|
5
|
+
module Ruby
|
6
|
+
class RefactorRuby < Custom
|
7
|
+
def run
|
8
|
+
logger.verbose "Custom refactor to #{input_file}... (using user supplied prompt #{prompt_file_path})"
|
9
|
+
logger.verbose "Write output to #{output_file_path}..." if output_file_path
|
10
|
+
|
11
|
+
begin
|
12
|
+
output_content = process!(strip_ticks: true)
|
13
|
+
rescue => e
|
14
|
+
logger.error "Failed to process #{input_file}: #{e.message}"
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
|
18
|
+
return false unless output_content
|
19
|
+
|
20
|
+
output_file_path ? true : output_content
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.description
|
24
|
+
"Generic refactor using user supplied prompt (assumes Ruby code generation)"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
module Refactors
|
5
|
+
module Ruby
|
6
|
+
class WriteRuby < Custom
|
7
|
+
def run
|
8
|
+
logger.verbose "Write some ruby code... (using user supplied prompt #{prompt_file_path})"
|
9
|
+
logger.verbose "Write output to #{output_file_path}..." if output_file_path
|
10
|
+
|
11
|
+
begin
|
12
|
+
output_content = process!(strip_ticks: true)
|
13
|
+
rescue => e
|
14
|
+
logger.error "Failed to process #{input_file}: #{e.message}"
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
|
18
|
+
return false unless output_content
|
19
|
+
|
20
|
+
output_file_path ? true : output_content
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.takes_input_files?
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.description
|
28
|
+
"User supplied prompt to write Ruby code"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -2,6 +2,11 @@
|
|
2
2
|
|
3
3
|
module AIRefactor
|
4
4
|
module Refactors
|
5
|
+
def register(klass)
|
6
|
+
all[klass.refactor_name] = klass
|
7
|
+
end
|
8
|
+
module_function :register
|
9
|
+
|
5
10
|
def get(name)
|
6
11
|
all[name]
|
7
12
|
end
|
@@ -12,16 +17,19 @@ module AIRefactor
|
|
12
17
|
end
|
13
18
|
module_function :names
|
14
19
|
|
15
|
-
def
|
16
|
-
|
17
|
-
hash[klass.refactor_name] = klass
|
18
|
-
end
|
20
|
+
def descriptions
|
21
|
+
names.map { |n| "\"#{n}\"" }.zip(all.values.map(&:description)).to_h
|
19
22
|
end
|
20
|
-
module_function :
|
23
|
+
module_function :descriptions
|
21
24
|
|
22
25
|
def supported?(name)
|
23
26
|
names.include?(name)
|
24
27
|
end
|
25
28
|
module_function :supported?
|
29
|
+
|
30
|
+
def all
|
31
|
+
@all ||= {}
|
32
|
+
end
|
33
|
+
module_function :all
|
26
34
|
end
|
27
35
|
end
|