ai_refactor 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +76 -0
- data/LICENSE.txt +21 -0
- data/README.md +86 -0
- data/Rakefile +14 -0
- data/ai_refactor.gemspec +35 -0
- data/exe/ai_refactor +102 -0
- data/lib/ai_refactor/file_processor.rb +79 -0
- data/lib/ai_refactor/logger.rb +36 -0
- data/lib/ai_refactor/refactors/generic.rb +45 -0
- data/lib/ai_refactor/refactors/minitest_to_rspec.rb +11 -0
- data/lib/ai_refactor/refactors/prompts/minitest_to_rspec.md +0 -0
- data/lib/ai_refactor/refactors/prompts/rspec_to_minitest_rails.md +233 -0
- data/lib/ai_refactor/refactors/rspec_to_minitest_rails.rb +103 -0
- data/lib/ai_refactor/refactors/tests/minitest_runner.rb +24 -0
- data/lib/ai_refactor/refactors/tests/rspec_runner.rb +26 -0
- data/lib/ai_refactor/refactors/tests/test_run_diff_report.rb +30 -0
- data/lib/ai_refactor/refactors/tests/test_run_result.rb +28 -0
- data/lib/ai_refactor/refactors.rb +27 -0
- data/lib/ai_refactor/version.rb +5 -0
- data/lib/ai_refactor.rb +11 -0
- metadata +118 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: db3b53eb0cea61e83d0d7f5632316be07ced02da11e065626cc5d72329191538
|
4
|
+
data.tar.gz: 9b475208c15ccfaf9185d8c516e62efc28632dc2a6dab6d25beaba28be726278
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 97e8c839f7c5e2dd6fc1edc12b6c46f45e99e07701da53c313c7677a159529dffd91a724cf54cb33b0c51d1b84939fb07e94c9dae516906a72030b30f3c9fcaa
|
7
|
+
data.tar.gz: cd4250db8c71efad4daf61ac551b930904a103b573c595cbc3f2a8be7d6b9947576b00bce52f9c8f5858962802eb8827959b8aba5d32193a3732acfb7a108aeb
|
data/.standard.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
ai_refactor (0.1.0)
|
5
|
+
colorize (< 2.0)
|
6
|
+
open3 (< 2.0)
|
7
|
+
ruby-openai (>= 3.4.0, < 5.0)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
ast (2.4.2)
|
13
|
+
colorize (0.8.1)
|
14
|
+
faraday (2.7.4)
|
15
|
+
faraday-net_http (>= 2.0, < 3.1)
|
16
|
+
ruby2_keywords (>= 0.0.4)
|
17
|
+
faraday-multipart (1.0.4)
|
18
|
+
multipart-post (~> 2)
|
19
|
+
faraday-net_http (3.0.2)
|
20
|
+
json (2.6.3)
|
21
|
+
language_server-protocol (3.17.0.3)
|
22
|
+
lint_roller (1.0.0)
|
23
|
+
minitest (5.18.0)
|
24
|
+
multipart-post (2.3.0)
|
25
|
+
open3 (0.1.2)
|
26
|
+
parallel (1.23.0)
|
27
|
+
parser (3.2.2.1)
|
28
|
+
ast (~> 2.4.1)
|
29
|
+
rainbow (3.1.1)
|
30
|
+
rake (13.0.6)
|
31
|
+
regexp_parser (2.8.0)
|
32
|
+
rexml (3.2.5)
|
33
|
+
rubocop (1.50.2)
|
34
|
+
json (~> 2.3)
|
35
|
+
parallel (~> 1.10)
|
36
|
+
parser (>= 3.2.0.0)
|
37
|
+
rainbow (>= 2.2.2, < 4.0)
|
38
|
+
regexp_parser (>= 1.8, < 3.0)
|
39
|
+
rexml (>= 3.2.5, < 4.0)
|
40
|
+
rubocop-ast (>= 1.28.0, < 2.0)
|
41
|
+
ruby-progressbar (~> 1.7)
|
42
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
43
|
+
rubocop-ast (1.28.1)
|
44
|
+
parser (>= 3.2.1.0)
|
45
|
+
rubocop-performance (1.16.0)
|
46
|
+
rubocop (>= 1.7.0, < 2.0)
|
47
|
+
rubocop-ast (>= 0.4.0)
|
48
|
+
ruby-openai (4.1.0)
|
49
|
+
faraday (>= 1)
|
50
|
+
faraday-multipart (>= 1)
|
51
|
+
ruby-progressbar (1.13.0)
|
52
|
+
ruby2_keywords (0.0.5)
|
53
|
+
standard (1.28.2)
|
54
|
+
language_server-protocol (~> 3.17.0.2)
|
55
|
+
lint_roller (~> 1.0)
|
56
|
+
rubocop (~> 1.50.2)
|
57
|
+
standard-custom (~> 1.0.0)
|
58
|
+
standard-performance (~> 1.0.1)
|
59
|
+
standard-custom (1.0.0)
|
60
|
+
lint_roller (~> 1.0)
|
61
|
+
standard-performance (1.0.1)
|
62
|
+
lint_roller (~> 1.0)
|
63
|
+
rubocop-performance (~> 1.16.0)
|
64
|
+
unicode-display_width (2.4.2)
|
65
|
+
|
66
|
+
PLATFORMS
|
67
|
+
arm64-darwin-22
|
68
|
+
|
69
|
+
DEPENDENCIES
|
70
|
+
ai_refactor!
|
71
|
+
minitest (~> 5.0)
|
72
|
+
rake (~> 13.0)
|
73
|
+
standard (~> 1.3)
|
74
|
+
|
75
|
+
BUNDLED WITH
|
76
|
+
2.4.10
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Stephen Ierodiaconou
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# AI Refactor
|
2
|
+
|
3
|
+
AI Refactor is an experimental tool to see how AI (specifically [OpenAI's ChatGPT](https://platform.openai.com/)) can be used to help apply refactoring to code.
|
4
|
+
|
5
|
+
The goal is **not** that the AI decides what refactoring to do, but rather, given refactoring tasks specified by the human user,
|
6
|
+
the AI can help identify which code to change and apply the relevant refactor.
|
7
|
+
|
8
|
+
This is based on the assumption that the LLM AIs are pretty good at identifying patterns.
|
9
|
+
|
10
|
+
## Available refactors
|
11
|
+
|
12
|
+
Currently only one is available:
|
13
|
+
|
14
|
+
### `rspec_to_minitest_rails`
|
15
|
+
|
16
|
+
Converts RSpec tests to minitest tests for Rails test suites (ie generated minitest tests are actually `ActiveSupport::TestCase`s).
|
17
|
+
|
18
|
+
The tool first runs the original RSpec spec file and then runs the generated minitest test file, and compares the output of both.
|
19
|
+
|
20
|
+
The comparison is simply the count of successful and failed tests but this is probably enough to determine if the conversion worked.
|
21
|
+
|
22
|
+
```shellq
|
23
|
+
stephen$ OPENAI_API_KEY=my-key ai_refactor rspec_to_minitest_rails spec/models/my_thing_spec.rb -v
|
24
|
+
AI Refactor 1 files(s)/dir(s) '["spec/models/my_thing_spec.rb"]' with rspec_to_minitest_rails refactor
|
25
|
+
====================
|
26
|
+
Processing spec/models/my_thing_spec.rb...
|
27
|
+
[Run spec spec/models/my_thing_spec.rb... (bundle exec rspec spec/models/my_thing_spec.rb)]
|
28
|
+
Do you wish to overwrite test/models/company_buyer_test.rb? (y/n)
|
29
|
+
y
|
30
|
+
[Converting spec/models/my_thing_spec.rb...]
|
31
|
+
[Generate AI output. Generation attempts left: 3]
|
32
|
+
[OpenAI finished, with reason 'stop'...]
|
33
|
+
[Used tokens: 1869]
|
34
|
+
[Converted spec/models/my_thing_spec.rb to test/models/company_buyer_test.rb...]
|
35
|
+
[Run generated test file test/models/company_buyer_test.rb (bundle exec rails test test/models/company_buyer_test.rb)...]
|
36
|
+
[Done converting spec/models/my_thing_spec.rb to test/models/company_buyer_test.rb...]
|
37
|
+
No differences found! Conversion worked!
|
38
|
+
Refactor succeeded on spec/models/my_thing_spec.rb
|
39
|
+
|
40
|
+
Done processing all files!
|
41
|
+
```
|
42
|
+
|
43
|
+
## Installation
|
44
|
+
|
45
|
+
Install the gem and add to the application's Gemfile by executing:
|
46
|
+
|
47
|
+
$ bundle add ai_refactor
|
48
|
+
|
49
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
50
|
+
|
51
|
+
$ gem install ai_refactor
|
52
|
+
|
53
|
+
## Usage
|
54
|
+
|
55
|
+
See `ai_refactor --help` for more information.
|
56
|
+
|
57
|
+
```
|
58
|
+
Usage: ai_refactor REFACTOR_TYPE INPUT_FILE_OR_DIR [options]
|
59
|
+
|
60
|
+
Where REFACTOR_TYPE is one of: ["generic", "rspec_to_minitest_rails", "minitest_to_rspec"]
|
61
|
+
|
62
|
+
-p, --prompt PROMPT_FILE Specify path to a text file that contains the ChatGPT 'system' prompt.
|
63
|
+
-c, --continue [MAX_MESSAGES] If ChatGPT stops generating due to the maximum token count being reached, continue to generate more messages, until a stop condition or MAX_MESSAGES. MAX_MESSAGES defaults to 3
|
64
|
+
-m, --model MODEL_NAME Specify a ChatGPT model to use (default gpt-3.5-turbo).
|
65
|
+
--temperature TEMP Specify the temperature parameter for ChatGPT (default 0.7).
|
66
|
+
--max-tokens MAX_TOKENS Specify the max number of tokens of output ChatGPT can generate. Max will depend on the size of the prompt (default 1500)
|
67
|
+
-t, --timeout SECONDS Specify the max wait time for ChatGPT response.
|
68
|
+
-v, --verbose Show extra output and progress info
|
69
|
+
-d, --debug Show debugging output to help diagnose issues
|
70
|
+
-h, --help Prints this help
|
71
|
+
```
|
72
|
+
|
73
|
+
|
74
|
+
## Development
|
75
|
+
|
76
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
77
|
+
|
78
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
79
|
+
|
80
|
+
## Contributing
|
81
|
+
|
82
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/stevegeek/ai_refactor.
|
83
|
+
|
84
|
+
## License
|
85
|
+
|
86
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
require "standard/rake"
|
13
|
+
|
14
|
+
task default: %i[test standard]
|
data/ai_refactor.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/ai_refactor/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "ai_refactor"
|
7
|
+
spec.version = AIRefactor::VERSION
|
8
|
+
spec.authors = ["Stephen Ierodiaconou"]
|
9
|
+
spec.email = ["stevegeek@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Use AI to convert a Rails RSpec test suite to minitest."
|
12
|
+
spec.description = "Use OpenAI's ChatGPT to automate converting Rails RSpec tests to minitest (ActiveSupport::TestCase)."
|
13
|
+
spec.homepage = "https://github.com/stevegeek/ai_refactor"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.7.0"
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/stevegeek/ai_refactor"
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
24
|
+
(File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
spec.bindir = "exe"
|
28
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
29
|
+
spec.require_paths = ["lib"]
|
30
|
+
|
31
|
+
# Uncomment to register a new dependency of your gem
|
32
|
+
spec.add_dependency "colorize", "< 2.0"
|
33
|
+
spec.add_dependency "open3", "< 2.0"
|
34
|
+
spec.add_dependency "ruby-openai", ">= 3.4.0", "< 5.0"
|
35
|
+
end
|
data/exe/ai_refactor
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "colorize"
|
5
|
+
require "openai"
|
6
|
+
require_relative "../lib/ai_refactor"
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
|
10
|
+
supported_refactors = AIRefactor::Refactors.all
|
11
|
+
supported_names = AIRefactor::Refactors.names
|
12
|
+
|
13
|
+
# General options for all refactor types
|
14
|
+
option_parser = OptionParser.new do |parser|
|
15
|
+
parser.banner = "Usage: ai_refactor REFACTOR_TYPE INPUT_FILE_OR_DIR [options]\n\nWhere REFACTOR_TYPE is one of: #{supported_names}\n\n"
|
16
|
+
|
17
|
+
# todo: support for a sort of generic process which uses a custom prompt file
|
18
|
+
parser.on("-p", "--prompt PROMPT_FILE", String, "Specify path to a text file that contains the ChatGPT 'system' prompt.") do |f|
|
19
|
+
options[:prompt_file_path] = f
|
20
|
+
end
|
21
|
+
|
22
|
+
parser.on("-c", "--continue [MAX_MESSAGES]", Integer, "If ChatGPT stops generating due to the maximum token count being reached, continue to generate more messages, until a stop condition or MAX_MESSAGES. MAX_MESSAGES defaults to 3") do |c|
|
23
|
+
options[:ai_max_attempts] = c || 3
|
24
|
+
end
|
25
|
+
|
26
|
+
parser.on("-m", "--model MODEL_NAME", String, "Specify a ChatGPT model to use (default gpt-3.5-turbo).") do |m|
|
27
|
+
options[:ai_model] = m
|
28
|
+
end
|
29
|
+
|
30
|
+
parser.on(nil, "--temperature TEMP", Float, "Specify the temperature parameter for ChatGPT (default 0.7).") do |p|
|
31
|
+
options[:ai_temperature] = p
|
32
|
+
end
|
33
|
+
|
34
|
+
parser.on(nil, "--max-tokens MAX_TOKENS", Integer, "Specify the max number of tokens of output ChatGPT can generate. Max will depend on the size of the prompt (default 1500)") do |m|
|
35
|
+
options[:ai_max_tokens] = m
|
36
|
+
end
|
37
|
+
|
38
|
+
parser.on("-t", "--timeout SECONDS", Integer, "Specify the max wait time for ChatGPT response.") do |m|
|
39
|
+
options[:ai_timeout] = m
|
40
|
+
end
|
41
|
+
|
42
|
+
parser.on("-v", "--verbose", "Show extra output and progress info") do
|
43
|
+
options[:verbose] = true
|
44
|
+
end
|
45
|
+
|
46
|
+
parser.on("-d", "--debug", "Show debugging output to help diagnose issues") do
|
47
|
+
options[:debug] = true
|
48
|
+
end
|
49
|
+
|
50
|
+
supported_refactors.each do |_name, refactorer|
|
51
|
+
refactorer.command_line_options.each do |option|
|
52
|
+
parser.on(option[:short], option[:long], option[:type], option[:help]) do |o|
|
53
|
+
options[option[:key]] = o
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
parser.on("-h", "--help", "Prints this help") do
|
59
|
+
puts parser
|
60
|
+
exit
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
option_parser.parse!
|
65
|
+
|
66
|
+
logger = AIRefactor::Logger.new(verbose: options[:verbose], debug: options[:debug])
|
67
|
+
|
68
|
+
refactoring_type = ARGV.shift
|
69
|
+
input_file_path = ARGV
|
70
|
+
|
71
|
+
if !AIRefactor::Refactors.supported?(refactoring_type) || input_file_path.nil? || input_file_path.empty?
|
72
|
+
puts option_parser.help
|
73
|
+
exit 1
|
74
|
+
end
|
75
|
+
|
76
|
+
OpenAI.configure do |config|
|
77
|
+
config.access_token = ENV.fetch("OPENAI_API_KEY")
|
78
|
+
config.organization_id = ENV.fetch("OPENAI_ORGANIZATION_ID", nil)
|
79
|
+
config.request_timeout = options[:ai_timeout] || 240
|
80
|
+
end
|
81
|
+
|
82
|
+
refactorer = AIRefactor::Refactors.get(refactoring_type)
|
83
|
+
|
84
|
+
inputs = input_file_path.map do |path|
|
85
|
+
File.exist?(path) ? path : Dir.glob(path)
|
86
|
+
end.flatten
|
87
|
+
|
88
|
+
logger.info "AI Refactor #{inputs.size} files(s)/dir(s) '#{input_file_path}' with #{refactorer.refactor_name} refactor\n"
|
89
|
+
logger.info "====================\n"
|
90
|
+
|
91
|
+
inputs.each do |file|
|
92
|
+
logger.info "Processing #{file}..."
|
93
|
+
|
94
|
+
refactor = refactorer.new(file, options, logger)
|
95
|
+
|
96
|
+
if refactor.run
|
97
|
+
logger.success "Refactor succeeded on #{file}\n"
|
98
|
+
else
|
99
|
+
logger.warn "Refactor failed on #{file}\n"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
logger.info "Done processing all files!"
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openai"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module AIRefactor
|
7
|
+
class FileProcessor
|
8
|
+
attr_reader :file_path, :output_path, :logger
|
9
|
+
|
10
|
+
def initialize(file_path, output_path, prompt_file_path:, ai_client:, logger:)
|
11
|
+
@file_path = file_path
|
12
|
+
@output_path = output_path
|
13
|
+
@prompt_file_path = prompt_file_path
|
14
|
+
@ai_client = ai_client
|
15
|
+
@logger = logger
|
16
|
+
end
|
17
|
+
|
18
|
+
def output_exists?
|
19
|
+
File.exist?(output_path)
|
20
|
+
end
|
21
|
+
|
22
|
+
def process!(options)
|
23
|
+
logger.debug("Processing #{file_path} with prompt in #{@prompt_file_path}")
|
24
|
+
prompt = File.read(@prompt_file_path)
|
25
|
+
input = File.read(@file_path)
|
26
|
+
messages = [
|
27
|
+
{role: "system", content: prompt},
|
28
|
+
{role: "user", content: "Convert: ```#{input}```"}
|
29
|
+
]
|
30
|
+
content, finished_reason, usage = generate_next_message(messages, prompt, options, options[:ai_max_attempts] || 3)
|
31
|
+
|
32
|
+
if content && content.length > 0
|
33
|
+
processed = block_given? ? yield(content) : content
|
34
|
+
File.write(output_path, processed)
|
35
|
+
end
|
36
|
+
|
37
|
+
[content, finished_reason, usage]
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def generate_next_message(messages, prompt, options, attempts_left)
|
43
|
+
logger.verbose "Generate AI output. Generation attempts left: #{attempts_left}"
|
44
|
+
logger.debug "Options: #{options.inspect}"
|
45
|
+
logger.debug "Messages: #{messages.inspect}"
|
46
|
+
|
47
|
+
response = @ai_client.chat(
|
48
|
+
parameters: {
|
49
|
+
model: options[:ai_model] || "gpt-3.5-turbo",
|
50
|
+
messages: messages,
|
51
|
+
temperature: options[:ai_temperature] || 0.7,
|
52
|
+
max_tokens: options[:ai_max_tokens] || 1500
|
53
|
+
}
|
54
|
+
)
|
55
|
+
|
56
|
+
if response["error"]
|
57
|
+
raise StandardError.new("OpenAI error: #{response["error"]["type"]}: #{response["error"]["message"]} (#{response["error"]["code"]})")
|
58
|
+
end
|
59
|
+
|
60
|
+
content = response.dig("choices", 0, "message", "content")
|
61
|
+
finished_reason = response.dig("choices", 0, "finish_reason")
|
62
|
+
|
63
|
+
if finished_reason == "length" && attempts_left > 0
|
64
|
+
generate_next_message(messages + [
|
65
|
+
{role: "assistant", content: content},
|
66
|
+
{role: "user", content: "Continue"}
|
67
|
+
], prompt, options, attempts_left - 1)
|
68
|
+
else
|
69
|
+
previous_messages = messages.filter { |m| m[:role] == "assistant" }.map { |m| m[:content] }.join
|
70
|
+
content = if previous_messages.length > 0
|
71
|
+
content ? previous_messages + content : previous_messages
|
72
|
+
else
|
73
|
+
content
|
74
|
+
end
|
75
|
+
[content, finished_reason, response["usage"]]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
class Logger
|
5
|
+
def initialize(verbose: false, debug: false)
|
6
|
+
@verbose = verbose
|
7
|
+
@debug = debug
|
8
|
+
end
|
9
|
+
|
10
|
+
def info(message)
|
11
|
+
puts message
|
12
|
+
end
|
13
|
+
|
14
|
+
def debug(message)
|
15
|
+
return unless @debug
|
16
|
+
puts message.colorize(:light_black)
|
17
|
+
end
|
18
|
+
|
19
|
+
def verbose(message)
|
20
|
+
return unless @verbose
|
21
|
+
puts "[#{message}]".colorize(:light_blue)
|
22
|
+
end
|
23
|
+
|
24
|
+
def warn(message)
|
25
|
+
puts message.colorize(:yellow)
|
26
|
+
end
|
27
|
+
|
28
|
+
def success(message)
|
29
|
+
puts message.colorize(color: :green, mode: :bold)
|
30
|
+
end
|
31
|
+
|
32
|
+
def error(message, bold: false)
|
33
|
+
puts message.colorize(color: :red, mode: bold ? :bold : :default)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
module Refactors
|
5
|
+
class Generic
|
6
|
+
attr_reader :input_file, :options, :logger
|
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 "Not implemented"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def ai_client
|
21
|
+
@ai_client ||= OpenAI::Client.new
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def command_line_options
|
26
|
+
[]
|
27
|
+
end
|
28
|
+
|
29
|
+
def refactor_name
|
30
|
+
name.split("::")
|
31
|
+
.last
|
32
|
+
.gsub(/::/, "/")
|
33
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
34
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
35
|
+
.tr("-", "_")
|
36
|
+
.downcase
|
37
|
+
end
|
38
|
+
|
39
|
+
def prompt_file_path
|
40
|
+
File.join(File.dirname(File.expand_path(__FILE__)), "prompts", "#{refactor_name}.md")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
File without changes
|
@@ -0,0 +1,233 @@
|
|
1
|
+
You convert RSpec tests to ActiveSupport::TestCase tests for Ruby on Rails.
|
2
|
+
ActiveSupport::TestCase uses MiniTest under the hood.
|
3
|
+
Remember that MiniTest does not support `context` blocks, instead these should be removed and the context
|
4
|
+
specified in them should be moved directly into the relevant tests.
|
5
|
+
Always enclose the output code in triple backticks (```).
|
6
|
+
|
7
|
+
Here are some examples to use as a guide:
|
8
|
+
|
9
|
+
Example 1) RSpec:
|
10
|
+
```
|
11
|
+
require "rails_helper"
|
12
|
+
|
13
|
+
RSpec.describe Address, type: :model do
|
14
|
+
subject(:model) { described_class.new }
|
15
|
+
|
16
|
+
it { is_expected.not_to have_many(:assigned_companies) }
|
17
|
+
it { is_expected.not_to belong_to(:delivery_location) }
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
Result 1) minitest:
|
22
|
+
```
|
23
|
+
require "test_helper"
|
24
|
+
|
25
|
+
class AddressTest < ActiveSupport::TestCase
|
26
|
+
@model = Address.new
|
27
|
+
|
28
|
+
test "model should not have any assigned_companies" do
|
29
|
+
assert_empty @model.assigned_companies
|
30
|
+
end
|
31
|
+
|
32
|
+
test "model should not have a delivery_location" do
|
33
|
+
refute @model.delivery_location
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
Example 2) RSpec:
|
39
|
+
```
|
40
|
+
subject(:model) { create(:order_state) }
|
41
|
+
|
42
|
+
context "when rejected" do
|
43
|
+
before { model.rejected_at = 1.day.ago }
|
44
|
+
|
45
|
+
context "with reason and message" do
|
46
|
+
before do
|
47
|
+
model.rejected_message = reason
|
48
|
+
model.rejected_reason = RejectedReasons.reason(:out_of_stock)
|
49
|
+
end
|
50
|
+
|
51
|
+
let(:reason) { "my reason" }
|
52
|
+
|
53
|
+
it "should be valid" do
|
54
|
+
expect(model).to be_valid
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should have a rejected message" do
|
58
|
+
expect(model.rejected_message).to eq reason
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
Result 2) minitest:
|
65
|
+
```
|
66
|
+
setup do
|
67
|
+
@model = FactoryBot.create(:order_state)
|
68
|
+
@reason = "my reason"
|
69
|
+
end
|
70
|
+
|
71
|
+
test "when rejected, with reason and message, model should be valid" do
|
72
|
+
@model.rejected_at = 1.day.ago
|
73
|
+
@model.rejected_message = @reason
|
74
|
+
@model.rejected_reason = RejectedReasons.reason(:out_of_stock)
|
75
|
+
assert @model.valid?
|
76
|
+
assert_equal @reason, @model.rejected_message
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
Example 3) RSpec:
|
81
|
+
```
|
82
|
+
RSpec.describe Address, type: :model do
|
83
|
+
subject(:model) { build_stubbed(:address, geo: point_1) }
|
84
|
+
|
85
|
+
let(:factory) { RGeo::Geographic.spherical_factory(srid: 4326) }
|
86
|
+
let(:point_1) { factory.point(-84.3804222, 33.6502466) }
|
87
|
+
let(:point_2) { factory.point(-84.00, 33.00) }
|
88
|
+
|
89
|
+
it { is_expected.to be_instance_of(described_class) }
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
Result 3) minitest:
|
94
|
+
```
|
95
|
+
class AddressTest < ActiveSupport::TestCase
|
96
|
+
setup do
|
97
|
+
@factory = RGeo::Geographic.spherical_factory(srid: 4326)
|
98
|
+
@point_1 = @factory.point(-84.3804222, 33.6502466)
|
99
|
+
@point_2 = @factory.point(-84.00, 33.00)
|
100
|
+
@model = FactoryBot.build_stubbed(:address, geo: @point_1)
|
101
|
+
end
|
102
|
+
|
103
|
+
test "model should be an instance of the Address" do
|
104
|
+
assert_instance_of Address, @model
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
Example 4) RSpec:
|
110
|
+
```
|
111
|
+
describe "geocoding" do
|
112
|
+
context "when address line changed" do
|
113
|
+
before { model.line_1 = "1 Test Road" }
|
114
|
+
|
115
|
+
it "geocodes when validated" do
|
116
|
+
model.validate
|
117
|
+
expect(model.geo).to eq point_2
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
Result 4) minitest:
|
124
|
+
```
|
125
|
+
test "model should geocode, when address line changed, and when validated" do
|
126
|
+
@model.line_1 = "1 Test Road"
|
127
|
+
@model.validate
|
128
|
+
assert_equal @point_2, @model.geo
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
Example 5) RSpec:
|
133
|
+
```
|
134
|
+
context "when address line changed" do
|
135
|
+
it "geocodes when address changed" do
|
136
|
+
expect(PointFromLatLng).to receive(:call).with(33.00, -84.00).and_call_original
|
137
|
+
model.validate
|
138
|
+
expect(model.geo).to eq point_2
|
139
|
+
end
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
Result 5) minitest:
|
144
|
+
```
|
145
|
+
test "model should geocode, when address line changed, and when address changed" do
|
146
|
+
mock = Minitest::Mock.new
|
147
|
+
mock.expect :call, @point_2, [33.00, -84.00]
|
148
|
+
|
149
|
+
PointFromLatLng.stub :call, mock do
|
150
|
+
@model.line_1 = "1 Test Road"
|
151
|
+
@model.validate
|
152
|
+
assert_equal @point_2, @model.geo
|
153
|
+
end
|
154
|
+
|
155
|
+
mock.verify
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
Example 6) RSpec:
|
160
|
+
```
|
161
|
+
context "when address line changed" do
|
162
|
+
describe "setting timezone" do
|
163
|
+
it "sets timezone on successful fetch" do
|
164
|
+
other = build(:address)
|
165
|
+
expect(model).to be_valid
|
166
|
+
expect(model.timezone).to eq "America/New_York"
|
167
|
+
end
|
168
|
+
|
169
|
+
it "sets default timezone on timezone error" do
|
170
|
+
allow(Timezone).to receive(:lookup).and_raise(Timezone::Error::Base)
|
171
|
+
expect(model).to be_valid
|
172
|
+
expect(model.timezone).to eq ::Config[:vendor_location_info][:timezone]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
```
|
177
|
+
|
178
|
+
Result 6) minitest:
|
179
|
+
```
|
180
|
+
test "when address line changed, model should set timezone on successful fetch" do
|
181
|
+
@model.line_1 = "1 Test Road"
|
182
|
+
other = FactoryBot.build(:address)
|
183
|
+
assert @model.valid?
|
184
|
+
assert_equal "America/New_York", @model.timezone
|
185
|
+
end
|
186
|
+
|
187
|
+
test "when address line changed, model should set default timezone on timezone error" do
|
188
|
+
@model.line_1 = "1 Test Road"
|
189
|
+
Timezone.stub :lookup, ->(*) { raise Timezone::Error::Base.new } do
|
190
|
+
assert @model.valid?
|
191
|
+
assert_equal ::Config[:vendor_location_info][:timezone], @model.timezone
|
192
|
+
end
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
Example 7) RSpec:
|
197
|
+
```
|
198
|
+
context "when address line untouched" do
|
199
|
+
it "does not geocode" do
|
200
|
+
expect(PointFromLatLng).not_to receive(:call)
|
201
|
+
expect(model).to be_valid
|
202
|
+
expect(model.geo).to eq point_1
|
203
|
+
end
|
204
|
+
end
|
205
|
+
```
|
206
|
+
|
207
|
+
Result 7) minitest:
|
208
|
+
```
|
209
|
+
test "when address line untouched, model should not geocode" do
|
210
|
+
PointFromLatLng.stub(:call, ->(*) { raise "shouldn't be called" }) do
|
211
|
+
assert @model.valid?
|
212
|
+
assert_equal @point_1, @model.geo
|
213
|
+
end
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
Example 8) RSpec:
|
218
|
+
```
|
219
|
+
it "stubs any instance" do
|
220
|
+
allow_any_instance_of(PointFromLatLng).to receive(:foo).and_return(true)
|
221
|
+
expect(model).to be_valid
|
222
|
+
end
|
223
|
+
|
224
|
+
```
|
225
|
+
|
226
|
+
Result 8) minitest:
|
227
|
+
```
|
228
|
+
test "stubs any instance" do
|
229
|
+
PointFromLatLng.stub_any_instance :foo, true do
|
230
|
+
assert @model.valid?
|
231
|
+
end
|
232
|
+
end
|
233
|
+
```
|
@@ -0,0 +1,103 @@
|
|
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 < Generic
|
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
|
+
return false
|
21
|
+
end
|
22
|
+
|
23
|
+
logger.debug "Original test run results:"
|
24
|
+
logger.debug ">> Examples: #{spec_run.example_count}, Failures: #{spec_run.failure_count}, Pendings: #{spec_run.pending_count}"
|
25
|
+
|
26
|
+
output_path = input_file.gsub("_spec.rb", "_test.rb").gsub("spec/", "test/")
|
27
|
+
|
28
|
+
processor = AIRefactor::FileProcessor.new(
|
29
|
+
input_file,
|
30
|
+
output_path,
|
31
|
+
prompt_file_path: self.class.prompt_file_path,
|
32
|
+
ai_client: ai_client,
|
33
|
+
logger: logger
|
34
|
+
)
|
35
|
+
|
36
|
+
if processor.output_exists?
|
37
|
+
logger.info "Do you wish to overwrite #{output_path}? (y/n)"
|
38
|
+
answer = $stdin.gets.chomp
|
39
|
+
unless answer == "y" || answer == "Y"
|
40
|
+
logger.warn "Skipping #{input_file}..."
|
41
|
+
return false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
logger.verbose "Converting #{input_file}..."
|
46
|
+
|
47
|
+
begin
|
48
|
+
output_content, finished_reason, usage = processor.process!(options) do |content|
|
49
|
+
content.gsub("```", "")
|
50
|
+
end
|
51
|
+
rescue => e
|
52
|
+
logger.error "Request to OpenAI failed: #{e.message}"
|
53
|
+
logger.warn "Skipping #{input_file}..."
|
54
|
+
return false
|
55
|
+
end
|
56
|
+
|
57
|
+
logger.verbose "OpenAI finished, with reason '#{finished_reason}'..."
|
58
|
+
logger.verbose "Used tokens: #{usage["total_tokens"]}".colorize(:light_black) if usage
|
59
|
+
|
60
|
+
if finished_reason == "length"
|
61
|
+
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."
|
62
|
+
logger.warn "Continuing to test the translated file... but it is likely to fail."
|
63
|
+
end
|
64
|
+
|
65
|
+
if !output_content || output_content.length == 0
|
66
|
+
logger.warn "Skipping #{input_file}, no translated output..."
|
67
|
+
logger.error "Failed to translate #{input_file}, finished reason #{finished_reason}"
|
68
|
+
return false
|
69
|
+
end
|
70
|
+
|
71
|
+
logger.verbose "Converted #{input_file} to #{output_path}..."
|
72
|
+
|
73
|
+
minitest_runner = AIRefactor::Tests::MinitestRunner.new(processor.output_path)
|
74
|
+
|
75
|
+
logger.verbose "Run generated test file #{output_path} (#{minitest_runner.command})..."
|
76
|
+
test_run = minitest_runner.run
|
77
|
+
|
78
|
+
if test_run.failed?
|
79
|
+
logger.warn "Skipping #{input_file}..."
|
80
|
+
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"
|
81
|
+
logger.error "Conversion failed!", bold: true
|
82
|
+
return false
|
83
|
+
end
|
84
|
+
|
85
|
+
logger.debug "Translated test file results:"
|
86
|
+
logger.debug ">> Runs: #{test_run.example_count}, Failures: #{test_run.failure_count}, Skips: #{test_run.pending_count}"
|
87
|
+
|
88
|
+
report = AIRefactor::Tests::TestRunDiffReport.new(spec_run, test_run)
|
89
|
+
|
90
|
+
if report.no_differences?
|
91
|
+
logger.verbose "Done converting #{input_file} to #{output_path}..."
|
92
|
+
logger.success "No differences found! Conversion worked!"
|
93
|
+
true
|
94
|
+
else
|
95
|
+
logger.warn report.diff.colorize(:yellow)
|
96
|
+
logger.verbose "Done converting #{input_file} to #{output_path}..."
|
97
|
+
logger.error "Differences found! Conversion failed!", bold: true
|
98
|
+
false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
module AIRefactor
|
6
|
+
module Tests
|
7
|
+
class MinitestRunner
|
8
|
+
def initialize(file_path, command_template: "bundle exec rails test __FILE__")
|
9
|
+
@file_path = file_path
|
10
|
+
@command_template = command_template
|
11
|
+
end
|
12
|
+
|
13
|
+
def command
|
14
|
+
@command_template.gsub("__FILE__", @file_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
stdout, stderr, status = Open3.capture3(command)
|
19
|
+
_matched, runs, _assertions, failures, errors, skips = stdout.match(/(\d+) runs, (\d+) assertions, (\d+) failures, (\d+) errors, (\d+) skips/).to_a
|
20
|
+
TestRunResult.new(stdout, stderr, status, runs, failures, skips, errors)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
module AIRefactor
|
6
|
+
module Tests
|
7
|
+
class RSpecRunner
|
8
|
+
def initialize(file_path, command_template: "bundle exec rspec __FILE__")
|
9
|
+
@file_path = file_path
|
10
|
+
@command_template = command_template
|
11
|
+
end
|
12
|
+
|
13
|
+
def command
|
14
|
+
@command_template.gsub("__FILE__", @file_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
stdout, stderr, status = Open3.capture3(command)
|
19
|
+
_matched, example_count, failure_count = stdout.match(/(\d+) examples?, (\d+) failures?/).to_a
|
20
|
+
pending_count = stdout.match(/(\d+) pending/)&.values_at(1) || "0"
|
21
|
+
errored = stdout.match(/, (\d+) errors? occurred outside of examples/)&.values_at(1) || "0"
|
22
|
+
TestRunResult.new(stdout, stderr, status, example_count, failure_count, pending_count, errored)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
module Tests
|
5
|
+
class TestRunDiffReport
|
6
|
+
def initialize(previous_test_run_result, test_run_result)
|
7
|
+
@current = test_run_result
|
8
|
+
@previous = previous_test_run_result
|
9
|
+
end
|
10
|
+
|
11
|
+
def no_differences?
|
12
|
+
@current.example_count == @previous.example_count && @current.failure_count == @previous.failure_count && @current.pending_count == @previous.pending_count
|
13
|
+
end
|
14
|
+
|
15
|
+
def diff
|
16
|
+
report = ""
|
17
|
+
if @current.example_count != @previous.example_count
|
18
|
+
report += "Example count mismatch: #{@current.example_count} != #{@previous.example_count}"
|
19
|
+
end
|
20
|
+
if @current.failure_count != @previous.failure_count
|
21
|
+
report += "Failure count mismatch: #{@current.failure_count} != #{@previous.failure_count}"
|
22
|
+
end
|
23
|
+
if @current.pending_count != @previous.pending_count
|
24
|
+
report += "Pending count mismatch: #{@current.pending_count} != #{@previous.pending_count}"
|
25
|
+
end
|
26
|
+
report
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
module Tests
|
5
|
+
class TestRunResult
|
6
|
+
attr_reader :stdout, :stderr, :example_count, :failure_count, :pending_count
|
7
|
+
|
8
|
+
def initialize(stdout, stderr, status, example_count, failure_count, pending_count, errored)
|
9
|
+
@stdout = stdout
|
10
|
+
@stderr = stderr
|
11
|
+
@status = status
|
12
|
+
@example_count = example_count
|
13
|
+
@failure_count = failure_count
|
14
|
+
@pending_count = pending_count
|
15
|
+
@errored = errored
|
16
|
+
end
|
17
|
+
|
18
|
+
def failed?
|
19
|
+
return true unless @status.success?
|
20
|
+
@errored && @errored.to_i > 0
|
21
|
+
end
|
22
|
+
|
23
|
+
def exitstatus
|
24
|
+
@status.exitstatus
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AIRefactor
|
4
|
+
module Refactors
|
5
|
+
def get(name)
|
6
|
+
all[name]
|
7
|
+
end
|
8
|
+
module_function :get
|
9
|
+
|
10
|
+
def names
|
11
|
+
all.keys
|
12
|
+
end
|
13
|
+
module_function :names
|
14
|
+
|
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
|
19
|
+
end
|
20
|
+
module_function :all
|
21
|
+
|
22
|
+
def supported?(name)
|
23
|
+
names.include?(name)
|
24
|
+
end
|
25
|
+
module_function :supported?
|
26
|
+
end
|
27
|
+
end
|
data/lib/ai_refactor.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "ai_refactor/version"
|
4
|
+
|
5
|
+
require_relative "ai_refactor/logger"
|
6
|
+
require_relative "ai_refactor/file_processor"
|
7
|
+
|
8
|
+
require_relative "ai_refactor/refactors"
|
9
|
+
require_relative "ai_refactor/refactors/generic"
|
10
|
+
require_relative "ai_refactor/refactors/rspec_to_minitest_rails"
|
11
|
+
require_relative "ai_refactor/refactors/minitest_to_rspec"
|
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ai_refactor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephen Ierodiaconou
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-05-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: colorize
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "<"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "<"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: open3
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "<"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "<"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: ruby-openai
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.4.0
|
48
|
+
- - "<"
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '5.0'
|
51
|
+
type: :runtime
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 3.4.0
|
58
|
+
- - "<"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '5.0'
|
61
|
+
description: Use OpenAI's ChatGPT to automate converting Rails RSpec tests to minitest
|
62
|
+
(ActiveSupport::TestCase).
|
63
|
+
email:
|
64
|
+
- stevegeek@gmail.com
|
65
|
+
executables:
|
66
|
+
- ai_refactor
|
67
|
+
extensions: []
|
68
|
+
extra_rdoc_files: []
|
69
|
+
files:
|
70
|
+
- ".standard.yml"
|
71
|
+
- CHANGELOG.md
|
72
|
+
- Gemfile
|
73
|
+
- Gemfile.lock
|
74
|
+
- LICENSE.txt
|
75
|
+
- README.md
|
76
|
+
- Rakefile
|
77
|
+
- ai_refactor.gemspec
|
78
|
+
- exe/ai_refactor
|
79
|
+
- lib/ai_refactor.rb
|
80
|
+
- lib/ai_refactor/file_processor.rb
|
81
|
+
- lib/ai_refactor/logger.rb
|
82
|
+
- lib/ai_refactor/refactors.rb
|
83
|
+
- lib/ai_refactor/refactors/generic.rb
|
84
|
+
- lib/ai_refactor/refactors/minitest_to_rspec.rb
|
85
|
+
- lib/ai_refactor/refactors/prompts/minitest_to_rspec.md
|
86
|
+
- lib/ai_refactor/refactors/prompts/rspec_to_minitest_rails.md
|
87
|
+
- lib/ai_refactor/refactors/rspec_to_minitest_rails.rb
|
88
|
+
- lib/ai_refactor/refactors/tests/minitest_runner.rb
|
89
|
+
- lib/ai_refactor/refactors/tests/rspec_runner.rb
|
90
|
+
- lib/ai_refactor/refactors/tests/test_run_diff_report.rb
|
91
|
+
- lib/ai_refactor/refactors/tests/test_run_result.rb
|
92
|
+
- lib/ai_refactor/version.rb
|
93
|
+
homepage: https://github.com/stevegeek/ai_refactor
|
94
|
+
licenses:
|
95
|
+
- MIT
|
96
|
+
metadata:
|
97
|
+
homepage_uri: https://github.com/stevegeek/ai_refactor
|
98
|
+
source_code_uri: https://github.com/stevegeek/ai_refactor
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: 2.7.0
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubygems_version: 3.4.10
|
115
|
+
signing_key:
|
116
|
+
specification_version: 4
|
117
|
+
summary: Use AI to convert a Rails RSpec test suite to minitest.
|
118
|
+
test_files: []
|