ai_refactor 0.3.0 → 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 +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,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.0"
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.0
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,70 +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
- self.class.prompt_file_path
37
- end
38
-
39
- def ai_client
40
- @ai_client ||= OpenAI::Client.new
41
- end
42
-
43
- class << self
44
- def command_line_options
45
- []
46
- end
47
-
48
- def refactor_name
49
- name.split("::")
50
- .last
51
- .gsub(/::/, "/")
52
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
53
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
54
- .tr("-", "_")
55
- .downcase
56
- end
57
-
58
- def prompt_file_path
59
- file = if options[:prompt_file_path]&.length&.positive?
60
- options[:prompt_file_path]
61
- else
62
- File.join(File.dirname(File.expand_path(__FILE__)), "prompts", "#{refactor_name}.md")
63
- end
64
- file.tap do |prompt|
65
- raise "No prompt file '#{prompt}' found for #{refactor_name}" unless File.exist?(prompt)
66
- end
67
- end
68
- end
69
- end
70
- 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