ai_refactor 0.3.1 → 0.4.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.
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 +63 -37
  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 -76
  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 -66
  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
@@ -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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AIRefactor
4
+ module Refactors
5
+ module Rspec
6
+ class MinitestToRspec < BaseRefactor
7
+ def run
8
+ raise "Not implemented"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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 all
16
- @all ||= constants.map { |n| const_get(n) }.select { |c| c.is_a? Class }.each_with_object({}) do |klass, hash|
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 :all
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
@@ -3,7 +3,7 @@
3
3
  require "open3"
4
4
 
5
5
  module AIRefactor
6
- module Tests
6
+ module TestRunners
7
7
  class MinitestRunner
8
8
  def initialize(file_path, command_template: "bundle exec rails test __FILE__")
9
9
  @file_path = file_path
@@ -3,7 +3,7 @@
3
3
  require "open3"
4
4
 
5
5
  module AIRefactor
6
- module Tests
6
+ module TestRunners
7
7
  class RSpecRunner
8
8
  def initialize(file_path, command_template: "bundle exec rspec __FILE__")
9
9
  @file_path = file_path
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AIRefactor
4
- module Tests
4
+ module TestRunners
5
5
  class TestRunDiffReport
6
6
  def initialize(previous_test_run_result, test_run_result)
7
7
  @current = test_run_result
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AIRefactor
4
- module Tests
4
+ module TestRunners
5
5
  class TestRunResult
6
6
  attr_reader :stdout, :stderr, :example_count, :failure_count, :pending_count
7
7
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AIRefactor
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/ai_refactor.rb CHANGED
@@ -1,12 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "ai_refactor/version"
3
+ require "zeitwerk"
4
+ loader = Zeitwerk::Loader.for_gem
5
+ loader.inflector.inflect(
6
+ "ai_refactor" => "AIRefactor",
7
+ "rspec_runner" => "RSpecRunner"
8
+ )
9
+ loader.setup # ready!
4
10
 
5
- require_relative "ai_refactor/logger"
6
- require_relative "ai_refactor/file_processor"
11
+ module AIRefactor
12
+ class NoOutputError < StandardError; end
13
+ # Your code goes here...
14
+ end
7
15
 
8
- require_relative "ai_refactor/refactors"
9
- require_relative "ai_refactor/base_refactor"
10
- require_relative "ai_refactor/refactors/generic"
11
- require_relative "ai_refactor/refactors/rspec_to_minitest_rails"
12
- require_relative "ai_refactor/refactors/minitest_to_rspec"
16
+ # We eager load here to ensure that all Refactor classes are loaded at startup so they can be registered
17
+ loader.eager_load
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ai_refactor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Ierodiaconou
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-25 00:00:00.000000000 Z
11
+ date: 2023-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -58,6 +58,20 @@ dependencies:
58
58
  - - "<"
59
59
  - !ruby/object:Gem::Version
60
60
  version: '5.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: zeitwerk
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.6'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.6'
61
75
  description: Use OpenAI's ChatGPT to automate converting Rails RSpec tests to minitest
62
76
  (ActiveSupport::TestCase).
63
77
  email:
@@ -77,19 +91,28 @@ files:
77
91
  - ai_refactor.gemspec
78
92
  - exe/ai_refactor
79
93
  - lib/ai_refactor.rb
80
- - lib/ai_refactor/base_refactor.rb
94
+ - lib/ai_refactor/cli.rb
95
+ - lib/ai_refactor/context.rb
81
96
  - lib/ai_refactor/file_processor.rb
82
97
  - lib/ai_refactor/logger.rb
98
+ - lib/ai_refactor/prompt.rb
99
+ - lib/ai_refactor/prompts/diff.md
100
+ - lib/ai_refactor/prompts/input.md
83
101
  - lib/ai_refactor/refactors.rb
102
+ - lib/ai_refactor/refactors/base_refactor.rb
84
103
  - lib/ai_refactor/refactors/generic.rb
85
- - lib/ai_refactor/refactors/minitest_to_rspec.rb
86
- - lib/ai_refactor/refactors/prompts/minitest_to_rspec.md
87
- - lib/ai_refactor/refactors/prompts/rspec_to_minitest_rails.md
88
- - lib/ai_refactor/refactors/rspec_to_minitest_rails.rb
89
- - lib/ai_refactor/refactors/tests/minitest_runner.rb
90
- - lib/ai_refactor/refactors/tests/rspec_runner.rb
91
- - lib/ai_refactor/refactors/tests/test_run_diff_report.rb
92
- - lib/ai_refactor/refactors/tests/test_run_result.rb
104
+ - lib/ai_refactor/refactors/minitest/write_test_for_class.md
105
+ - lib/ai_refactor/refactors/minitest/write_test_for_class.rb
106
+ - lib/ai_refactor/refactors/project/write_changelog_from_history.md
107
+ - lib/ai_refactor/refactors/project/write_changelog_from_history.rb
108
+ - lib/ai_refactor/refactors/rails/minitest/rspec_to_minitest.md
109
+ - lib/ai_refactor/refactors/rails/minitest/rspec_to_minitest.rb
110
+ - lib/ai_refactor/refactors/rspec/minitest_to_rspec.md
111
+ - lib/ai_refactor/refactors/rspec/minitest_to_rspec.rb
112
+ - lib/ai_refactor/test_runners/minitest_runner.rb
113
+ - lib/ai_refactor/test_runners/rspec_runner.rb
114
+ - lib/ai_refactor/test_runners/test_run_diff_report.rb
115
+ - lib/ai_refactor/test_runners/test_run_result.rb
93
116
  - lib/ai_refactor/version.rb
94
117
  homepage: https://github.com/stevegeek/ai_refactor
95
118
  licenses:
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AIRefactor
4
- class BaseRefactor
5
- attr_reader :input_file, :options, :logger
6
- attr_writer :failed_message
7
-
8
- def initialize(input_file, options, logger)
9
- @input_file = input_file
10
- @options = options
11
- @logger = logger
12
- end
13
-
14
- def run
15
- raise NotImplementedError
16
- end
17
-
18
- def failed_message
19
- @failed_message || "Reason not specified"
20
- end
21
-
22
- private
23
-
24
- def can_overwrite_output_file?(output_path)
25
- logger.info "Do you wish to overwrite #{output_path}? (y/n)"
26
- answer = $stdin.gets.chomp
27
- unless answer == "y" || answer == "Y"
28
- logger.warn "Skipping #{input_file}..."
29
- self.failed_message = "Skipped as output file already exists"
30
- return false
31
- end
32
- true
33
- end
34
-
35
- def prompt_file_path
36
- file = if options && options[:prompt_file_path]&.length&.positive?
37
- options[:prompt_file_path]
38
- else
39
- File.join(File.dirname(File.expand_path(__FILE__)), "refactors", "prompts", "#{refactor_name}.md")
40
- end
41
- file.tap do |prompt|
42
- raise "No prompt file '#{prompt}' found for #{refactor_name}" unless File.exist?(prompt)
43
- end
44
- end
45
-
46
- def ai_client
47
- @ai_client ||= OpenAI::Client.new
48
- end
49
-
50
- class << self
51
- def command_line_options
52
- []
53
- end
54
-
55
- def refactor_name
56
- name.split("::")
57
- .last
58
- .gsub(/::/, "/")
59
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
60
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
61
- .tr("-", "_")
62
- .downcase
63
- end
64
- end
65
- end
66
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AIRefactor
4
- module Refactors
5
- class MinitestToRspec < BaseRefactor
6
- def run
7
- raise "Not implemented"
8
- end
9
- end
10
- end
11
- end
@@ -1,103 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "tests/test_run_result"
4
- require_relative "tests/rspec_runner"
5
- require_relative "tests/minitest_runner"
6
- require_relative "tests/test_run_diff_report"
7
-
8
- module AIRefactor
9
- module Refactors
10
- class RspecToMinitestRails < BaseRefactor
11
- def run
12
- spec_runner = AIRefactor::Tests::RSpecRunner.new(input_file)
13
- logger.verbose "Run spec #{input_file}... (#{spec_runner.command})"
14
-
15
- spec_run = spec_runner.run
16
-
17
- if spec_run.failed?
18
- logger.warn "Skipping #{input_file}..."
19
- logger.error "Failed to run #{input_file}, exited with status #{spec_run.exitstatus}. Stdout: #{spec_run.stdout}\n\nStderr: #{spec_run.stderr}\n\n"
20
- self.failed_message = "Failed to run RSpec file, has errors"
21
- return false
22
- end
23
-
24
- logger.debug "Original test run results:"
25
- logger.debug ">> Examples: #{spec_run.example_count}, Failures: #{spec_run.failure_count}, Pendings: #{spec_run.pending_count}"
26
-
27
- output_path = input_file.gsub("_spec.rb", "_test.rb").gsub("spec/", "test/")
28
-
29
- processor = AIRefactor::FileProcessor.new(
30
- input_path: input_file,
31
- output_path: output_path,
32
- prompt_file_path: prompt_file_path,
33
- ai_client: ai_client,
34
- logger: logger
35
- )
36
-
37
- if processor.output_exists?
38
- return false unless can_overwrite_output_file?(output_path)
39
- end
40
-
41
- logger.verbose "Converting #{input_file}..."
42
-
43
- begin
44
- output_content, finished_reason, usage = processor.process!(options) do |content|
45
- content.gsub("```", "")
46
- end
47
- rescue => e
48
- logger.error "Request to OpenAI failed: #{e.message}"
49
- logger.warn "Skipping #{input_file}..."
50
- self.failed_message = "Request to OpenAI failed"
51
- return false
52
- end
53
-
54
- logger.verbose "OpenAI finished, with reason '#{finished_reason}'..."
55
- logger.verbose "Used tokens: #{usage["total_tokens"]}".colorize(:light_black) if usage
56
-
57
- if finished_reason == "length"
58
- 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."
59
- logger.warn "Continuing to test the translated file... but it is likely to fail."
60
- end
61
-
62
- if !output_content || output_content.length == 0
63
- logger.warn "Skipping #{input_file}, no translated output..."
64
- logger.error "Failed to translate #{input_file}, finished reason #{finished_reason}"
65
- self.failed_message = "AI conversion failed, no output was generated"
66
- return false
67
- end
68
-
69
- logger.verbose "Converted #{input_file} to #{output_path}..."
70
-
71
- minitest_runner = AIRefactor::Tests::MinitestRunner.new(processor.output_path)
72
-
73
- logger.verbose "Run generated test file #{output_path} (#{minitest_runner.command})..."
74
- test_run = minitest_runner.run
75
-
76
- if test_run.failed?
77
- logger.warn "Skipping #{input_file}..."
78
- logger.error "Failed to run translated #{output_path}, exited with status #{test_run.exitstatus}. Stdout: #{test_run.stdout}\n\nStderr: #{test_run.stderr}\n\n"
79
- logger.error "Conversion failed!", bold: true
80
- self.failed_message = "Generated test file failed to run correctly"
81
- return false
82
- end
83
-
84
- logger.debug "Translated test file results:"
85
- logger.debug ">> Runs: #{test_run.example_count}, Failures: #{test_run.failure_count}, Skips: #{test_run.pending_count}"
86
-
87
- report = AIRefactor::Tests::TestRunDiffReport.new(spec_run, test_run)
88
-
89
- if report.no_differences?
90
- logger.verbose "Done converting #{input_file} to #{output_path}..."
91
- logger.success "No differences found! Conversion worked!"
92
- true
93
- else
94
- logger.warn report.diff.colorize(:yellow)
95
- logger.verbose "Done converting #{input_file} to #{output_path}..."
96
- logger.error "Differences found! Conversion failed!", bold: true
97
- self.failed_message = "Generated test file run output did not match original RSpec spec run output"
98
- false
99
- end
100
- end
101
- end
102
- end
103
- end