vercon 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/.tool-versions +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +35 -0
- data/Rakefile +10 -0
- data/exe/vercon +15 -0
- data/lib/vercon/claude.rb +53 -0
- data/lib/vercon/cli.rb +15 -0
- data/lib/vercon/commands/generate.rb +195 -0
- data/lib/vercon/commands/init.rb +55 -0
- data/lib/vercon/config.rb +46 -0
- data/lib/vercon/factories.rb +65 -0
- data/lib/vercon/prompt.rb +108 -0
- data/lib/vercon/stdout.rb +39 -0
- data/lib/vercon/version.rb +5 -0
- data/lib/vercon.rb +13 -0
- data/sig/vercon.rbs +4 -0
- data/vercon.gemspec +44 -0
- metadata +189 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8472e1a2fafc85a47fa7fd66553ecb16dd051d867165ff3441b8a883cb62b007
|
4
|
+
data.tar.gz: 9bbcc1a5af63dc57d0d74a0b7d2a089cc78ec4b814868b858f3ce6a059bb881e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bf335632885d7cee074bb756448c501a8b06cf31d8090d29827b98b836cf3b7ca644163302ef048161fbc48c226bd8643397f68ad3a5e87f82907015a5285c69
|
7
|
+
data.tar.gz: abafa240143904a41c04b121b55b4fb16b2fb4d81d27ca3977733c7151f625b28febf50f4ff6604c2b263e66963a4e09207f8c842ba382378a439d3ca7d9307e
|
data/.rspec
ADDED
data/.standard.yml
ADDED
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 3.2.3
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Alex Beznos
|
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,35 @@
|
|
1
|
+
# Vercon
|
2
|
+
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
4
|
+
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/vercon`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
+
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
12
|
+
|
13
|
+
$ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
14
|
+
|
15
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
|
+
|
17
|
+
$ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Development
|
24
|
+
|
25
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
26
|
+
|
27
|
+
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).
|
28
|
+
|
29
|
+
## Contributing
|
30
|
+
|
31
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/vercon.
|
32
|
+
|
33
|
+
## License
|
34
|
+
|
35
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/exe/vercon
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# require "vercon"
|
5
|
+
# TODO: change back
|
6
|
+
require_relative '../lib/vercon'
|
7
|
+
|
8
|
+
cli = Dry::CLI.new(Vercon::CLI)
|
9
|
+
|
10
|
+
begin
|
11
|
+
cli.call
|
12
|
+
rescue Vercon::Error => e
|
13
|
+
$stderr.puts(e.message) # rubocop:disable Style/StderrPuts
|
14
|
+
exit(1)
|
15
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'httpx'
|
4
|
+
|
5
|
+
module Vercon
|
6
|
+
class Claude
|
7
|
+
BASE_URL = 'https://api.anthropic.com'
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
config = Vercon::Config.new
|
11
|
+
|
12
|
+
@api_token = config.token
|
13
|
+
@claude_model = config.claude_model
|
14
|
+
end
|
15
|
+
|
16
|
+
def submit(model: nil, system: nil, max_tokens: 4096, temperature: 0.2, stop_sequences: nil, user: nil, # rubocop:disable Metrics/ParameterLists
|
17
|
+
messages: nil)
|
18
|
+
body = {
|
19
|
+
model: model || @claude_model,
|
20
|
+
system: system,
|
21
|
+
max_tokens: max_tokens,
|
22
|
+
temperature: temperature,
|
23
|
+
stop_sequences: stop_sequences,
|
24
|
+
messages: messages || [{ role: 'user', content: user }]
|
25
|
+
}.reject { |_, v| v.nil? || v == '' }
|
26
|
+
|
27
|
+
client.post('/v1/messages', body: body.to_json).then { |res| prepare_response(res.json) }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def extra_headers
|
33
|
+
{ 'x-api-key' => @api_token, 'anthropic-version' => '2023-06-01' }
|
34
|
+
end
|
35
|
+
|
36
|
+
def client
|
37
|
+
@client ||=
|
38
|
+
HTTPX
|
39
|
+
.plugin(:retries)
|
40
|
+
.with(headers: { 'Content-Type' => 'application/json', 'Cache-Control' => 'no-cache' })
|
41
|
+
.with(headers: extra_headers)
|
42
|
+
.with(origin: BASE_URL)
|
43
|
+
.with(ssl: { alpn_protocols: %w[http/1.1] })
|
44
|
+
.with(timeout: { keep_alive_timeout: 180 })
|
45
|
+
end
|
46
|
+
|
47
|
+
def prepare_response(response)
|
48
|
+
return { error: response.dig('error', 'message') } if response['type'] == 'error'
|
49
|
+
|
50
|
+
{ text: response.dig('content', 0, 'text') }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/vercon/cli.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/cli'
|
4
|
+
|
5
|
+
require_relative 'commands/init'
|
6
|
+
require_relative 'commands/generate'
|
7
|
+
|
8
|
+
module Vercon
|
9
|
+
class CLI
|
10
|
+
extend Dry::CLI::Registry
|
11
|
+
|
12
|
+
register 'init', Commands::Init, aliases: ['i']
|
13
|
+
register 'generate', Commands::Generate, aliases: ['g']
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'prism'
|
4
|
+
require 'dry/files'
|
5
|
+
require 'tty-spinner'
|
6
|
+
require 'tty-pager'
|
7
|
+
require 'tty-editor'
|
8
|
+
|
9
|
+
module Vercon
|
10
|
+
module Commands
|
11
|
+
class Generate < Dry::CLI::Command
|
12
|
+
desc 'Generate test file'
|
13
|
+
|
14
|
+
argument :path, desc: 'Path to the ruby file'
|
15
|
+
|
16
|
+
option :edit_prompt, type: :boolean, default: false, aliases: ['e'],
|
17
|
+
desc: 'Edit prompt before submitting to claude'
|
18
|
+
option :output_path, type: :string, default: nil, aliases: ['o'],
|
19
|
+
desc: 'Path to save test file'
|
20
|
+
option :stdout, type: :boolean, default: false, aliases: ['s'],
|
21
|
+
desc: 'Output test file to stdout instead of writing to test file'
|
22
|
+
option :force, type: :boolean, default: false, aliases: ['f'],
|
23
|
+
desc: 'Force overwrite of existing test file'
|
24
|
+
option :open, type: :boolean, default: false, aliases: ['p'],
|
25
|
+
desc: 'Open test file in editor after generation'
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@config = Vercon::Config.new
|
29
|
+
@stdout = Vercon::Stdout.new
|
30
|
+
@files = Dry::Files.new
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def call(path: nil, **opts)
|
36
|
+
return unless can_generate?(path, opts)
|
37
|
+
|
38
|
+
output_path = opts[:output_path] || generate_test_file_path(path, opts)
|
39
|
+
return if output_path.nil?
|
40
|
+
|
41
|
+
current_test = files.exist?(output_path) ? files.read(output_path) : nil
|
42
|
+
|
43
|
+
result = generate_test_file(path, opts, current_test)
|
44
|
+
|
45
|
+
if opts[:stdout]
|
46
|
+
pager = TTY::Pager.new
|
47
|
+
pager.page(result)
|
48
|
+
return
|
49
|
+
end
|
50
|
+
|
51
|
+
if !opts[:force] && files.exist?(output_path) && stdout.no?("File already exists at \"#{output_path}\". Overwrite?")
|
52
|
+
return
|
53
|
+
end
|
54
|
+
|
55
|
+
files.write(output_path, result)
|
56
|
+
|
57
|
+
run_rubocop(output_path) if include_gem?('rubocop') || include_gem?('standard')
|
58
|
+
|
59
|
+
stdout.ok("Test file saved at \"#{output_path}\" 🥳")
|
60
|
+
|
61
|
+
return unless opts[:open]
|
62
|
+
|
63
|
+
TTY::Editor.new(raise_on_failure: true).open(output_path)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
attr_reader :config, :stdout, :files
|
69
|
+
|
70
|
+
def can_generate?(path, _opts)
|
71
|
+
unless config.exists?
|
72
|
+
stdout.error('Config file does not exist. Run `vercon init` to create a config file.')
|
73
|
+
return false
|
74
|
+
end
|
75
|
+
|
76
|
+
if path.nil? || path.empty?
|
77
|
+
stdout.error('Path to ruby file is blank.')
|
78
|
+
return false
|
79
|
+
end
|
80
|
+
|
81
|
+
unless files.exist?(path)
|
82
|
+
stdout.error('Ruby file does not exist.')
|
83
|
+
return false
|
84
|
+
end
|
85
|
+
|
86
|
+
expanded_path = files.expand_path(path)
|
87
|
+
|
88
|
+
if Prism.parse_file_failure?(expanded_path)
|
89
|
+
stdout.error('Looks like the ruby file has syntax errors. Fix them before generating tests.')
|
90
|
+
return false
|
91
|
+
end
|
92
|
+
|
93
|
+
unless include_gem?('rspec')
|
94
|
+
stdout.error('RSpec is not installed. Vercon requires RSpec to generate test files.')
|
95
|
+
return false
|
96
|
+
end
|
97
|
+
|
98
|
+
true
|
99
|
+
end
|
100
|
+
|
101
|
+
def generate_test_file_path(path, _opts)
|
102
|
+
system, user, stop_sequence = Vercon::Prompt.for_test_path(path: path)
|
103
|
+
spinner = TTY::Spinner.new('[:spinner] Preparing spec file path...', format: :flip)
|
104
|
+
spinner.auto_spin
|
105
|
+
|
106
|
+
result = Vercon::Claude.new.submit(
|
107
|
+
model: config.class::LOWEST_CLAUDE_MODEL,
|
108
|
+
system: system, user: user,
|
109
|
+
stop_sequences: [stop_sequence]
|
110
|
+
)
|
111
|
+
spinner.stop
|
112
|
+
stdout.erase(lines: 1)
|
113
|
+
|
114
|
+
if result.key?(:error)
|
115
|
+
stdout.error("Claude returned error: #{result[:error]}")
|
116
|
+
return
|
117
|
+
end
|
118
|
+
|
119
|
+
path = result[:text].match(/RSPEC FILE PATH: "(.+)"/)[1]
|
120
|
+
|
121
|
+
if stdout.no?("Corresponding test file path should be \"#{path}\". Correct?")
|
122
|
+
path = stdout.ask('Enter a relative path of corresponding test:')
|
123
|
+
end
|
124
|
+
|
125
|
+
path
|
126
|
+
end
|
127
|
+
|
128
|
+
def generate_test_file(path, opts, current_test)
|
129
|
+
factories = Vercon::Factories.new.load if include_gem?('factory_bot')
|
130
|
+
system, user, stop_sequence = Vercon::Prompt.for_test_generation(
|
131
|
+
path: path, source: files.read(path),
|
132
|
+
factories: factories, current_test: current_test
|
133
|
+
)
|
134
|
+
system, user = ask_for_edits(system, user) if opts[:edit_prompt]
|
135
|
+
return if system.nil? || user.nil?
|
136
|
+
|
137
|
+
spinner = TTY::Spinner.new('[:spinner] Generating spec file...', format: :flip)
|
138
|
+
spinner.auto_spin
|
139
|
+
|
140
|
+
result = Vercon::Claude.new.submit(system: system, user: user, stop_sequences: [stop_sequence])
|
141
|
+
spinner.stop
|
142
|
+
stdout.erase(lines: 1)
|
143
|
+
|
144
|
+
if result.key?(:error)
|
145
|
+
stdout.error("Claude returned error: #{result[:error]}")
|
146
|
+
return
|
147
|
+
end
|
148
|
+
|
149
|
+
result[:text].match(/TEST SOURCE CODE:\n```ruby\n(.+)\n```/m)[1]
|
150
|
+
end
|
151
|
+
|
152
|
+
def run_rubocop(path)
|
153
|
+
spinner = TTY::Spinner.new('[:spinner] Running RuboCop...', format: :flip)
|
154
|
+
spinner.auto_spin
|
155
|
+
|
156
|
+
system("bundle exec rubocop -A #{files.expand_path(path)} > /dev/null 2>&1")
|
157
|
+
|
158
|
+
spinner.stop
|
159
|
+
stdout.erase(lines: 1)
|
160
|
+
end
|
161
|
+
|
162
|
+
def ask_for_edits(system, user)
|
163
|
+
path = '~/.vercon_prompt.txt'
|
164
|
+
text = <<~EOF.strip
|
165
|
+
Please, do not remove magick comments :)
|
166
|
+
<System prompt>
|
167
|
+
#{system}
|
168
|
+
<User prompt>
|
169
|
+
#{user}
|
170
|
+
EOF
|
171
|
+
|
172
|
+
files.write(path, text)
|
173
|
+
|
174
|
+
TTY::Editor.new(raise_on_failure: true).open(path)
|
175
|
+
TTY::Spinner.new('[:spinner] Waiting for changes...', format: :flip).run { sleep(rand(1..3)) }
|
176
|
+
stdout.erase(lines: 1)
|
177
|
+
|
178
|
+
if stdout.no?('Can we proceed?')
|
179
|
+
stdout.error('Generation aborted!')
|
180
|
+
return []
|
181
|
+
end
|
182
|
+
|
183
|
+
system, user = files.read(path).match(/<System prompt>\n(.+)\n<User prompt>\n(.+)/m).captures
|
184
|
+
|
185
|
+
[system, user]
|
186
|
+
ensure
|
187
|
+
files.delete(path)
|
188
|
+
end
|
189
|
+
|
190
|
+
def include_gem?(name)
|
191
|
+
files.read('Gemfile').include?(name)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vercon
|
4
|
+
module Commands
|
5
|
+
class Init < Dry::CLI::Command
|
6
|
+
desc 'Initialize vercon config'
|
7
|
+
|
8
|
+
option :token, desc: 'Claude API token'
|
9
|
+
option :claude_model, desc: 'Claude model to use by default'
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@stdout = Vercon::Stdout.new
|
13
|
+
|
14
|
+
@config = Vercon::Config.new
|
15
|
+
@config_existed = @config.exists?
|
16
|
+
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(**opts)
|
21
|
+
token_changed = setup_token(opts)
|
22
|
+
claude_changed = setup_claude_model(opts)
|
23
|
+
|
24
|
+
if token_changed || claude_changed
|
25
|
+
@stdout.ok("Config file #{@config_existed ? 'updated' : 'created'}!")
|
26
|
+
else
|
27
|
+
@stdout.warn('Config file is not touched.')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def setup_token(opts)
|
34
|
+
if @config.token && @stdout.no?("Claude API token already set to `#{@config.token}`. Do you want to replace it?")
|
35
|
+
return
|
36
|
+
end
|
37
|
+
|
38
|
+
token = opts[:token]
|
39
|
+
token ||= @stdout.ask('Provide your Claude API token:')
|
40
|
+
@config.token = token
|
41
|
+
end
|
42
|
+
|
43
|
+
def setup_claude_model(opts)
|
44
|
+
if @config.claude_model && @stdout.no?("Claude default model already set to `#{@config.claude_model}`. Do you want to replace it?")
|
45
|
+
return
|
46
|
+
end
|
47
|
+
|
48
|
+
model = opts[:claude_model]
|
49
|
+
model ||= @stdout.select('Select Claude model that will be used by default:', Vercon::Config::CLAUDE_MODELS,
|
50
|
+
default: Vercon::Config::DEFAULT_CLAUDE_MODEL, cycle: true)
|
51
|
+
@config.claude_model = model
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'dry/files'
|
5
|
+
|
6
|
+
module Vercon
|
7
|
+
class Config
|
8
|
+
CLAUDE_MODELS = %w[
|
9
|
+
claude-3-haiku-20240307
|
10
|
+
claude-3-sonnet-20240229
|
11
|
+
claude-3-opus-20240229
|
12
|
+
].freeze
|
13
|
+
DEFAULT_CLAUDE_MODEL = 'claude-3-sonnet-20240229'
|
14
|
+
LOWEST_CLAUDE_MODEL = 'claude-3-haiku-20240307'
|
15
|
+
PATH = '~/.vercon.yml'
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@files = Dry::Files.new
|
19
|
+
@config = YAML.load_file(@files.expand_path(PATH))
|
20
|
+
rescue Errno::ENOENT
|
21
|
+
@config = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def exists?
|
25
|
+
!@config.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
def token
|
29
|
+
@config['claude_token']
|
30
|
+
end
|
31
|
+
|
32
|
+
def token=(value)
|
33
|
+
@config['claude_token'] = value
|
34
|
+
@files.write(@files.expand_path(PATH), YAML.safe_dump(@config))
|
35
|
+
end
|
36
|
+
|
37
|
+
def claude_model
|
38
|
+
@config['claude_model']
|
39
|
+
end
|
40
|
+
|
41
|
+
def claude_model=(value)
|
42
|
+
@config['claude_model'] = value
|
43
|
+
@files.write(@files.expand_path(PATH), YAML.safe_dump(@config))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/files'
|
4
|
+
require 'prism'
|
5
|
+
|
6
|
+
module Vercon
|
7
|
+
class Factories
|
8
|
+
PATH = './spec/factories'
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@files = Dry::Files.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def load
|
15
|
+
return unless @files.directory?(PATH)
|
16
|
+
|
17
|
+
Dir[@files.expand_path(@files.join(PATH, '**', '*.rb'))].map do |file_path|
|
18
|
+
load_factory(file_path)
|
19
|
+
end.flatten.compact
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def load_factory(file_path)
|
25
|
+
factories = []
|
26
|
+
|
27
|
+
tree = Prism.parse_file(file_path)
|
28
|
+
return if tree.failure?
|
29
|
+
|
30
|
+
factory_node = find_factory_node(tree.value)
|
31
|
+
return unless factory_node
|
32
|
+
|
33
|
+
factory_node.block.body.body.each do |node|
|
34
|
+
factories << parse_factory(node) if node.type == :call_node && node.name == :factory
|
35
|
+
end
|
36
|
+
|
37
|
+
factories
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_factory_node(node)
|
41
|
+
case node.type
|
42
|
+
when :call_node
|
43
|
+
node if node.name == :define && node.receiver.name == :FactoryBot
|
44
|
+
when :program_node, :statements_node
|
45
|
+
node.child_nodes.map { |inner_node| find_factory_node(inner_node) }.find(&:itself)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_factory(node)
|
50
|
+
factory = {
|
51
|
+
name: node.arguments.child_nodes.first.unescaped
|
52
|
+
}
|
53
|
+
|
54
|
+
traits = node.block.body.child_nodes.map do |cnode|
|
55
|
+
next unless cnode.type == :call_node && cnode.name == :trait
|
56
|
+
|
57
|
+
cnode.arguments.child_nodes.first.unescaped
|
58
|
+
end.compact
|
59
|
+
|
60
|
+
factory[:traits] = traits unless traits.empty?
|
61
|
+
|
62
|
+
factory
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vercon
|
4
|
+
class Prompt
|
5
|
+
class << self
|
6
|
+
END_SEQUENCE = '<END RESULT>'
|
7
|
+
|
8
|
+
def for_test_path(path:)
|
9
|
+
system_prompt = <<~PROMPT.strip
|
10
|
+
You are tasked as a professional Ruby and Ruby on Rails developer specialising in writing comprehensive RSpec unit tests for a Ruby class. You will receive a path to ruby file. Your objective is to generate a path corresponding RSpec unit test file. Make sure to use common practices used in Ruby on Rails community for structuring test file paths.
|
11
|
+
|
12
|
+
Provide a in the following format:
|
13
|
+
RUBY FILE PATH: "<expected path that user provided>"
|
14
|
+
RSPEC FILE PATH: "<expected file path for RSpec file according to best practices>"
|
15
|
+
#{END_SEQUENCE}
|
16
|
+
|
17
|
+
Make sure to include "#{END_SEQUENCE}" at the end of your test source code. It's required.
|
18
|
+
PROMPT
|
19
|
+
|
20
|
+
user_prompt = <<~PROMPT.strip
|
21
|
+
PATH: #{path.inspect}
|
22
|
+
PROMPT
|
23
|
+
|
24
|
+
[system_prompt, user_prompt, END_SEQUENCE]
|
25
|
+
end
|
26
|
+
|
27
|
+
def for_test_generation(path:, source:, factories: nil, current_test: nil)
|
28
|
+
system_prompt = <<~PROMPT.strip
|
29
|
+
You are a professional Ruby developer specializing in writing comprehensive RSpec unit tests for a Ruby class. Your objective is to ensure each public method within the class is thoroughly tested. Below are the specifics you must adhere to:
|
30
|
+
|
31
|
+
- Structure:
|
32
|
+
- Organize your tests using `describe` and `context` blocks, ensuring there's a clear separation for each public method.
|
33
|
+
- Use meaningful descriptions for each test scenario to enhance readability.
|
34
|
+
|
35
|
+
- Coverage:
|
36
|
+
- Achieve at least 95% coverage by testing all possible outcomes, including both success and failure cases.
|
37
|
+
- For methods that can raise exceptions or handle errors, include tests that cover these scenarios.
|
38
|
+
|
39
|
+
- Assertions:
|
40
|
+
- Ensure your tests assert against all aspects of the method's behavior, including its return values, side effects (e.g., file operations), and state changes within the class.
|
41
|
+
- Use appropriate RSpec matchers to make assertions clear and expressive.
|
42
|
+
|
43
|
+
- Best Practices:
|
44
|
+
- Write clean, maintainable tests, avoiding redundancy and ensuring each test is self-contained.
|
45
|
+
- Utilize RSpec's features like `let`, `before`, and `after` blocks to set up preconditions and clean up after tests as needed.
|
46
|
+
- Follow RSpec naming conventions and style guidelines for consistency.
|
47
|
+
|
48
|
+
- Mocking and Stubbing:
|
49
|
+
- When necessary, use RSpec's built-in mocking and stubbing capabilities to isolate the class under test from its dependencies.
|
50
|
+
- This allows for focused testing of the class's behavior without relying on external components.
|
51
|
+
- Use mocks and stubs judiciously to avoid over-mocking and maintain the integrity of the tests.
|
52
|
+
|
53
|
+
- Factories:
|
54
|
+
- If the user provides an available factories list and the class interacts with a database or external services, use factories to create test data and simulate real-world scenarios.
|
55
|
+
- Ensure that only factories provided by the user are used and make sure they are defined correctly and provide the necessary data for the tests.
|
56
|
+
|
57
|
+
- Edge Cases and Boundary Conditions:
|
58
|
+
- Consider and test edge cases and boundary conditions that may affect the behavior of the class.
|
59
|
+
- This includes testing with empty or nil values, large or small input values, and any other relevant scenarios specific to the class.
|
60
|
+
- Think critically about potential edge cases and ensure they are adequately covered in the tests.
|
61
|
+
|
62
|
+
Provide the result in the following format:
|
63
|
+
|
64
|
+
TEST SOURCE CODE:
|
65
|
+
```ruby
|
66
|
+
<the RSpec tests, without any additional comments or markdown instructions>
|
67
|
+
```
|
68
|
+
#{END_SEQUENCE}
|
69
|
+
|
70
|
+
Make sure to include "#{END_SEQUENCE}" at the end of your test source code.
|
71
|
+
|
72
|
+
If the user provides "CURRENT RSPEC FILE", use it as a base for your tests, improve it, and extend it according to the Ruby file being tested. Ensure that the existing tests are updated to meet the specified requirements and that new tests are added to achieve comprehensive coverage of the class's public methods.
|
73
|
+
|
74
|
+
Remember to focus on testing the behavior of the class rather than its implementation details. Aim for concise, readable, and maintainable tests that provide confidence in the correctness of the class. Use descriptive test names, keep tests isolated from each other, and follow RSpec best practices and conventions throughout your test suite.
|
75
|
+
PROMPT
|
76
|
+
|
77
|
+
user_prompt = ["PATH: #{path.inspect}"]
|
78
|
+
|
79
|
+
if factories
|
80
|
+
user_prompt << <<~PROMPT.strip
|
81
|
+
AVAILABLE FACTORIES:
|
82
|
+
```json
|
83
|
+
#{JSON.dump(factories)}
|
84
|
+
```
|
85
|
+
PROMPT
|
86
|
+
end
|
87
|
+
|
88
|
+
user_prompt << <<~PROMPT.strip
|
89
|
+
CODE:
|
90
|
+
```ruby
|
91
|
+
#{source}
|
92
|
+
```
|
93
|
+
PROMPT
|
94
|
+
|
95
|
+
if current_test
|
96
|
+
user_prompt << <<~PROMPT.strip
|
97
|
+
CURRENT RSPEC FILE:
|
98
|
+
```ruby
|
99
|
+
#{current_test}
|
100
|
+
```
|
101
|
+
PROMPT
|
102
|
+
end
|
103
|
+
|
104
|
+
[system_prompt, user_prompt.join("\n"), END_SEQUENCE]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tty-prompt'
|
4
|
+
|
5
|
+
module Vercon
|
6
|
+
class Stdout
|
7
|
+
def initialize
|
8
|
+
@stdout = $stdout
|
9
|
+
@prompt = TTY::Prompt.new
|
10
|
+
@lines = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def write(message)
|
14
|
+
@stdout.puts(message)
|
15
|
+
@lines += 1
|
16
|
+
end
|
17
|
+
|
18
|
+
def erase(lines: nil)
|
19
|
+
(lines || @lines).times do
|
20
|
+
@stdout.print("\e[A\e[K")
|
21
|
+
@lines -= 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
%i[ask yes? no? say ok warn error mask select].each do |method|
|
26
|
+
define_method(method) do |*args, &block|
|
27
|
+
@lines += 1
|
28
|
+
@prompt.send(method, *args, &block)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
%i[puts print].each do |method|
|
33
|
+
define_method(method) do |*args, &block|
|
34
|
+
@lines += 1
|
35
|
+
@stdout.send(method, *args, &block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/vercon.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'vercon/version'
|
4
|
+
require_relative 'vercon/config'
|
5
|
+
require_relative 'vercon/stdout'
|
6
|
+
require_relative 'vercon/cli'
|
7
|
+
require_relative 'vercon/claude'
|
8
|
+
require_relative 'vercon/prompt'
|
9
|
+
require_relative 'vercon/factories'
|
10
|
+
|
11
|
+
module Vercon
|
12
|
+
class Error < StandardError; end
|
13
|
+
end
|
data/sig/vercon.rbs
ADDED
data/vercon.gemspec
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/vercon/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'vercon'
|
7
|
+
spec.version = Vercon::VERSION
|
8
|
+
spec.authors = ['Alex Beznos']
|
9
|
+
spec.email = ['beznosa@yahoo.com']
|
10
|
+
|
11
|
+
spec.summary = 'CLI tool to generate test files with Cloude 3'
|
12
|
+
spec.description = 'CLI tool to generate test files with Cloude 3.'
|
13
|
+
spec.homepage = 'https://github.com/AlexBeznoss/vercon'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = '>= 3.0'
|
16
|
+
spec.required_rubygems_version = '>= 3.3.11'
|
17
|
+
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
19
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(File.expand_path(f) == __FILE__) ||
|
26
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
spec.bindir = 'exe'
|
30
|
+
spec.executables = [spec.name]
|
31
|
+
spec.require_paths = ['lib']
|
32
|
+
|
33
|
+
spec.add_dependency 'dry-cli', '~> 1.0', '< 2'
|
34
|
+
spec.add_dependency 'dry-files', '~> 1.0', '< 2'
|
35
|
+
spec.add_dependency 'httpx', '~> 1.2.3'
|
36
|
+
spec.add_dependency 'prism', '~> 0.24'
|
37
|
+
spec.add_dependency 'tty-editor', '~> 0.7.0'
|
38
|
+
spec.add_dependency 'tty-pager', '~> 0.14'
|
39
|
+
spec.add_dependency 'tty-prompt', '~> 0.23.1'
|
40
|
+
spec.add_dependency 'tty-spinner', '~> 0.9.3'
|
41
|
+
|
42
|
+
# For more information and examples about making a new gem, check out our
|
43
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vercon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Beznos
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-04-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-cli
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '2'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: dry-files
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.0'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '2'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '1.0'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '2'
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: httpx
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 1.2.3
|
60
|
+
type: :runtime
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 1.2.3
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: prism
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0.24'
|
74
|
+
type: :runtime
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0.24'
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: tty-editor
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 0.7.0
|
88
|
+
type: :runtime
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - "~>"
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: 0.7.0
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: tty-pager
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - "~>"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0.14'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - "~>"
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0.14'
|
109
|
+
- !ruby/object:Gem::Dependency
|
110
|
+
name: tty-prompt
|
111
|
+
requirement: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - "~>"
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: 0.23.1
|
116
|
+
type: :runtime
|
117
|
+
prerelease: false
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - "~>"
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: 0.23.1
|
123
|
+
- !ruby/object:Gem::Dependency
|
124
|
+
name: tty-spinner
|
125
|
+
requirement: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - "~>"
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: 0.9.3
|
130
|
+
type: :runtime
|
131
|
+
prerelease: false
|
132
|
+
version_requirements: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - "~>"
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: 0.9.3
|
137
|
+
description: CLI tool to generate test files with Cloude 3.
|
138
|
+
email:
|
139
|
+
- beznosa@yahoo.com
|
140
|
+
executables:
|
141
|
+
- vercon
|
142
|
+
extensions: []
|
143
|
+
extra_rdoc_files: []
|
144
|
+
files:
|
145
|
+
- ".rspec"
|
146
|
+
- ".standard.yml"
|
147
|
+
- ".tool-versions"
|
148
|
+
- LICENSE.txt
|
149
|
+
- README.md
|
150
|
+
- Rakefile
|
151
|
+
- exe/vercon
|
152
|
+
- lib/vercon.rb
|
153
|
+
- lib/vercon/claude.rb
|
154
|
+
- lib/vercon/cli.rb
|
155
|
+
- lib/vercon/commands/generate.rb
|
156
|
+
- lib/vercon/commands/init.rb
|
157
|
+
- lib/vercon/config.rb
|
158
|
+
- lib/vercon/factories.rb
|
159
|
+
- lib/vercon/prompt.rb
|
160
|
+
- lib/vercon/stdout.rb
|
161
|
+
- lib/vercon/version.rb
|
162
|
+
- sig/vercon.rbs
|
163
|
+
- vercon.gemspec
|
164
|
+
homepage: https://github.com/AlexBeznoss/vercon
|
165
|
+
licenses:
|
166
|
+
- MIT
|
167
|
+
metadata:
|
168
|
+
homepage_uri: https://github.com/AlexBeznoss/vercon
|
169
|
+
source_code_uri: https://github.com/AlexBeznoss/vercon
|
170
|
+
post_install_message:
|
171
|
+
rdoc_options: []
|
172
|
+
require_paths:
|
173
|
+
- lib
|
174
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
175
|
+
requirements:
|
176
|
+
- - ">="
|
177
|
+
- !ruby/object:Gem::Version
|
178
|
+
version: '3.0'
|
179
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
180
|
+
requirements:
|
181
|
+
- - ">="
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: 3.3.11
|
184
|
+
requirements: []
|
185
|
+
rubygems_version: 3.4.19
|
186
|
+
signing_key:
|
187
|
+
specification_version: 4
|
188
|
+
summary: CLI tool to generate test files with Cloude 3
|
189
|
+
test_files: []
|