ai_refactor 0.1.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.
- 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: []
|