ghostest 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/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.rubocop.yml +236 -0
- data/.ruby-version +1 -0
- data/README.md +31 -0
- data/Rakefile +4 -0
- data/exe/ghostest +74 -0
- data/ghostest.gemspec +48 -0
- data/lib/ghostest/attr_reader.rb +11 -0
- data/lib/ghostest/config/agent.rb +30 -0
- data/lib/ghostest/config.rb +65 -0
- data/lib/ghostest/config_error.rb +5 -0
- data/lib/ghostest/error.rb +5 -0
- data/lib/ghostest/languages/ruby.rb +21 -0
- data/lib/ghostest/logger.rb +32 -0
- data/lib/ghostest/manager.rb +72 -0
- data/lib/ghostest/test_condition.rb +24 -0
- data/lib/ghostest/version.rb +3 -0
- data/lib/ghostest.rb +58 -0
- data/lib/google_custom_search.rb +30 -0
- data/lib/i18n_translator.rb +66 -0
- data/lib/initializers/i18n.rb +9 -0
- data/lib/llm/agents/base.rb +31 -0
- data/lib/llm/agents/reviewer.rb +50 -0
- data/lib/llm/agents/test_designer.rb +43 -0
- data/lib/llm/agents/test_programmer.rb +45 -0
- data/lib/llm/clients/azure_open_ai.rb +15 -0
- data/lib/llm/clients/base.rb +88 -0
- data/lib/llm/clients/open_ai.rb +14 -0
- data/lib/llm/functions/add_to_memory.rb +41 -0
- data/lib/llm/functions/base.rb +13 -0
- data/lib/llm/functions/exec_rspec_test.rb +39 -0
- data/lib/llm/functions/fix_one_rspec_test.rb +55 -0
- data/lib/llm/functions/get_files_list.rb +29 -0
- data/lib/llm/functions/get_gem_files_list.rb +43 -0
- data/lib/llm/functions/make_new_file.rb +43 -0
- data/lib/llm/functions/overwrite_file.rb +42 -0
- data/lib/llm/functions/read_file.rb +43 -0
- data/lib/llm/functions/record_lgtm.rb +48 -0
- data/lib/llm/functions/report_bug.rb +34 -0
- data/lib/llm/functions/switch_assignee.rb +74 -0
- data/lib/llm/message_container.rb +63 -0
- data/sig/ghostest.rbs +4 -0
- metadata +245 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8a4a9310232014fdaa2d5e34849f9c13ded1299abd8a387aa328b8a2b61bc038
|
4
|
+
data.tar.gz: 0dff83e6d82516c850c0cc4fae7ca9d80e9646e359bce5a2d0cd5f56f33584c1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 03cc41ffe2425f76e538ad3453576dc3c76a129810e7ad736e9df3402cd4ecc8ef200b2fd9a2e85dde169e5bd36170829e11f075eee9aa1029d08939460f4bec
|
7
|
+
data.tar.gz: 610c551d9d65f45aa0facb75949aa930ef401c85e2aaebbbb4a3b4b21361a0c677ea156b3dc1513887cc8e4f40dd6ff325ecdd48d537ca66ec385d4945626f9b
|
data/.idea/misc.xml
ADDED
data/.idea/modules.xml
ADDED
data/.idea/vcs.xml
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
|
2
|
+
AllCops:
|
3
|
+
NewCops: enable
|
4
|
+
TargetRubyVersion: 3.2.1
|
5
|
+
DisabledByDefault: true
|
6
|
+
DisplayCopNames: true
|
7
|
+
|
8
|
+
########################
|
9
|
+
# Indentation
|
10
|
+
########################
|
11
|
+
|
12
|
+
# [MUST] Use two spaces for 1-level of indent. Do not use the horizontal tab character.
|
13
|
+
Layout/IndentationStyle:
|
14
|
+
EnforcedStyle: spaces
|
15
|
+
Layout/IndentationWidth:
|
16
|
+
Enabled: true
|
17
|
+
Layout/IndentationConsistency:
|
18
|
+
Enabled: true
|
19
|
+
Layout/InitialIndentation:
|
20
|
+
Enabled: true
|
21
|
+
Layout/CommentIndentation:
|
22
|
+
Enabled: true
|
23
|
+
|
24
|
+
########################
|
25
|
+
# Whitespace
|
26
|
+
########################
|
27
|
+
|
28
|
+
# [MUST] Do not put whitespace at the end of a line.
|
29
|
+
Layout/TrailingWhitespace:
|
30
|
+
Enabled: true
|
31
|
+
|
32
|
+
########################
|
33
|
+
# Empty lines
|
34
|
+
########################
|
35
|
+
|
36
|
+
# [MUST] Leave exactly one newline at the end of a file.
|
37
|
+
Layout/TrailingEmptyLines:
|
38
|
+
EnforcedStyle: final_newline
|
39
|
+
|
40
|
+
########################
|
41
|
+
# Numbers
|
42
|
+
########################
|
43
|
+
|
44
|
+
# [SHOULD] Use underscores to separate every three-digits when writing long numbers.
|
45
|
+
Style/NumericLiterals:
|
46
|
+
MinDigits: 7
|
47
|
+
Strict: true
|
48
|
+
|
49
|
+
########################
|
50
|
+
# Strings
|
51
|
+
########################
|
52
|
+
|
53
|
+
# [SHOULD] Use `''` to write empty strings.
|
54
|
+
Style/EmptyLiteral:
|
55
|
+
Enabled: true
|
56
|
+
|
57
|
+
# [SHOULD] Use parentheses to write strings by `%` notation. You can use any kind of parentheses. In the following cases you can use non-parentheses characters for punctuations.
|
58
|
+
Style/PercentLiteralDelimiters:
|
59
|
+
Enabled: false
|
60
|
+
|
61
|
+
# [MUST] Do not write only `Object#to_s` in string interpolation, such as `"#{obj.to_s}"`.
|
62
|
+
Style/RedundantInterpolation:
|
63
|
+
Enabled: true
|
64
|
+
|
65
|
+
########################
|
66
|
+
# Arrays
|
67
|
+
########################
|
68
|
+
|
69
|
+
# [MUST] If you write an array literal just after an assignment operator such as `=`, obey the following form denoted as "good".
|
70
|
+
Layout/MultilineArrayBraceLayout:
|
71
|
+
EnforcedStyle: symmetrical
|
72
|
+
|
73
|
+
# [SHOULD] In the multi-line array literal, put `,` after the last item.
|
74
|
+
Style/TrailingCommaInArrayLiteral:
|
75
|
+
EnforcedStyleForMultiline: consistent_comma
|
76
|
+
|
77
|
+
# [SHOULD] Use general delimited input (percent) syntax `%w(...)` or `%W(...)` for word arrays.
|
78
|
+
Style/WordArray:
|
79
|
+
EnforcedStyle: percent
|
80
|
+
|
81
|
+
########################
|
82
|
+
# Hashes
|
83
|
+
########################
|
84
|
+
|
85
|
+
# [MUST] Put whitespaces between `{` and the first key, and between the last value and `}` when writing hash literals on a single line.
|
86
|
+
Layout/SpaceInsideHashLiteralBraces:
|
87
|
+
EnforcedStyle: space
|
88
|
+
|
89
|
+
# [MUST] Use new hash syntax in Ruby 1.9+ (`{ foo: 42 }`) if all keys can be written in that syntax:
|
90
|
+
Style/HashSyntax:
|
91
|
+
EnforcedStyle: ruby19
|
92
|
+
|
93
|
+
# [MUST] If you write a hash literal just after an assignment operator, such as `=`, obey the following form denoted as "good".
|
94
|
+
# [SHOULD] In the multi line hash literal, put `,` after the last item.
|
95
|
+
Style/TrailingCommaInHashLiteral:
|
96
|
+
EnforcedStyleForMultiline: consistent_comma
|
97
|
+
|
98
|
+
# [SHOULD] (Ruby 1.9+) If all the keys of hash literals are Symbol literals, use the form of `{ key: value }`. Put whitespace after `:`.
|
99
|
+
Layout/SpaceAfterColon:
|
100
|
+
Enabled: true
|
101
|
+
|
102
|
+
########################
|
103
|
+
# Operations
|
104
|
+
########################
|
105
|
+
|
106
|
+
# [SHOULD] Put whitespace around operators, except for `**`.
|
107
|
+
Layout/SpaceAroundOperators:
|
108
|
+
Enabled: true
|
109
|
+
|
110
|
+
# [MUST] Do not use `and`, `or`, and `not`.
|
111
|
+
Style/AndOr:
|
112
|
+
Enabled: true
|
113
|
+
|
114
|
+
# [MUST] Do not nest conditional operators.
|
115
|
+
Style/NestedTernaryOperator:
|
116
|
+
Enabled: true
|
117
|
+
|
118
|
+
# [MUST] Do not write conditional operators over multiple lines.
|
119
|
+
Style/MultilineTernaryOperator:
|
120
|
+
Enabled: true
|
121
|
+
|
122
|
+
########################
|
123
|
+
# Assignments
|
124
|
+
########################
|
125
|
+
|
126
|
+
# [MUST] Parallel assignments can only be used for assigning literal values or results of methods without arguments, and for exchanging two variables or attributes.
|
127
|
+
Style/ParallelAssignment:
|
128
|
+
Enabled: true
|
129
|
+
|
130
|
+
########################
|
131
|
+
# Control structures
|
132
|
+
########################
|
133
|
+
|
134
|
+
# [SHOULD] Use `unless condition`, instead of `if !condition`.
|
135
|
+
Style/NegatedIf:
|
136
|
+
Enabled: true
|
137
|
+
|
138
|
+
# [SHOULD] Use `until condition`, instead of `while !condition`.
|
139
|
+
Style/NegatedWhile:
|
140
|
+
Enabled: true
|
141
|
+
|
142
|
+
# [SHOULD] Do not use `else` for `unless`.
|
143
|
+
Style/UnlessElse:
|
144
|
+
Enabled: true
|
145
|
+
|
146
|
+
# [MUST] Do not use `then` and `:` for the condition clause of `if`, `unless`, and `case`.
|
147
|
+
Style/WhenThen:
|
148
|
+
Enabled: true
|
149
|
+
|
150
|
+
# [MUST] Do not use `do` and `:` for the condition clause of `while` and `until`.
|
151
|
+
Style/WhileUntilDo:
|
152
|
+
Enabled: true
|
153
|
+
|
154
|
+
# [SHOULD] Do not write a logical expressions combined by `||` in the condition clause of `unless` and `until`.
|
155
|
+
# [SHOULD] Use modifier forms, if conditions and bodies are short.
|
156
|
+
Style/WhileUntilModifier:
|
157
|
+
Enabled: true
|
158
|
+
|
159
|
+
########################
|
160
|
+
# Method calls
|
161
|
+
########################
|
162
|
+
|
163
|
+
# [MUST] Use brace block for a method call written in one line.
|
164
|
+
Style/BlockDelimiters:
|
165
|
+
EnforcedStyle: line_count_based
|
166
|
+
|
167
|
+
# [MUST] Put a whitespace before `{` of brace blocks.
|
168
|
+
Layout/SpaceBeforeBlockBraces:
|
169
|
+
EnforcedStyle: space
|
170
|
+
|
171
|
+
# [MUST] For a brace block written in one line, put whitespace between `{`, `}` and the inner contents.
|
172
|
+
Layout/SpaceInsideBlockBraces:
|
173
|
+
EnforcedStyle: space
|
174
|
+
|
175
|
+
########################
|
176
|
+
# BEGIN AND END
|
177
|
+
########################
|
178
|
+
|
179
|
+
# [MUST] Do not use `BEGIN` and `END` blocks.
|
180
|
+
Style/BeginBlock:
|
181
|
+
Enabled: true
|
182
|
+
Style/EndBlock:
|
183
|
+
Enabled: true
|
184
|
+
|
185
|
+
########################
|
186
|
+
# Module and Class definitions
|
187
|
+
########################
|
188
|
+
|
189
|
+
# [MUST] Use `alias_method` instead of `alias` to define aliases of methods.
|
190
|
+
Style/Alias:
|
191
|
+
EnforcedStyle: prefer_alias_method
|
192
|
+
|
193
|
+
# [MUST] use `attr_accessor`, `attr_reader`, and `attr_writer` to define accessors instead of `attr`.
|
194
|
+
Style/Attr:
|
195
|
+
Enabled: true
|
196
|
+
|
197
|
+
# [MUST] In definitions of class methods, use `self.` prefix of method name to reduce the indentation level. However, it is fine to use `class << self` when you want to define both public and private class methods.
|
198
|
+
Style/ClassMethods:
|
199
|
+
Enabled: true
|
200
|
+
|
201
|
+
# [MUST] If you use `private`, `protected`, and `public` without any arguments, align the lines of these method calls to their associated method definition. Put empty lines around the visibility-change methods.
|
202
|
+
Style/TrailingBodyOnMethodDefinition:
|
203
|
+
Enabled: true
|
204
|
+
|
205
|
+
########################
|
206
|
+
# Method definitions
|
207
|
+
########################
|
208
|
+
|
209
|
+
# [MUST] On method definition, do not omit parentheses of parameter list, except for methods without parameters.
|
210
|
+
Style/MethodDefParentheses:
|
211
|
+
Enabled: true
|
212
|
+
|
213
|
+
# [MUST] Do not put whitespace between method name and the parameter list.
|
214
|
+
Layout/SpaceAfterMethodName:
|
215
|
+
Enabled: true
|
216
|
+
|
217
|
+
########################
|
218
|
+
# Variables
|
219
|
+
########################
|
220
|
+
|
221
|
+
# [MUST] Do not introduce new global variables (`$foo`) for any reason.
|
222
|
+
Style/GlobalVars:
|
223
|
+
Enabled: true
|
224
|
+
|
225
|
+
# [MUST] Do not use class variables (`@@foo`) for any reasons. Use `class_attribute` instead.
|
226
|
+
Style/ClassVars:
|
227
|
+
Enabled: true
|
228
|
+
|
229
|
+
Lint/Debugger:
|
230
|
+
Enabled: true
|
231
|
+
|
232
|
+
Style/FrozenStringLiteralComment:
|
233
|
+
Enabled: false
|
234
|
+
|
235
|
+
Layout/CaseIndentation:
|
236
|
+
EnforcedStyle: end
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.2.1
|
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Ghostest
|
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/ghostest`. 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. 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]/ghostest.
|
data/Rakefile
ADDED
data/exe/ghostest
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib = File.expand_path('../lib', __dir__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
|
7
|
+
require "i18n"
|
8
|
+
config = File.expand_path('../config', __dir__)
|
9
|
+
I18n.load_path += Dir["#{config}/locales/**/*.yml"]
|
10
|
+
|
11
|
+
require 'ghostest'
|
12
|
+
require 'optparse'
|
13
|
+
require 'fileutils'
|
14
|
+
|
15
|
+
$stdout.sync = true
|
16
|
+
$stderr.sync = true
|
17
|
+
|
18
|
+
config_path = nil
|
19
|
+
|
20
|
+
options = {
|
21
|
+
llm: :open_ai,
|
22
|
+
debug: false,
|
23
|
+
}
|
24
|
+
|
25
|
+
ARGV.options do |opt|
|
26
|
+
opt.on('-c', '--config') { |v| config_path = v }
|
27
|
+
opt.on('-a', '--use-azure') { options[:llm] = :azure_open_ai }
|
28
|
+
opt.on('', '--debug') { options[:debug] = true }
|
29
|
+
|
30
|
+
opt.on('-v', '--version') do
|
31
|
+
puts Ghostest::VERSION
|
32
|
+
exit
|
33
|
+
end
|
34
|
+
|
35
|
+
opt.on('-h', '--help') do
|
36
|
+
puts opt.help
|
37
|
+
exit 0
|
38
|
+
end
|
39
|
+
|
40
|
+
opt.parse!
|
41
|
+
|
42
|
+
rescue StandardError => e
|
43
|
+
warn("[#{e.class.name}] #{e.message}")
|
44
|
+
puts "\t" + e.backtrace.join("\n\t") unless e.is_a?(OptionParser::ParseError)
|
45
|
+
exit 1
|
46
|
+
end
|
47
|
+
|
48
|
+
begin
|
49
|
+
required_envs = case options[:llm]
|
50
|
+
when :azure_open_ai
|
51
|
+
%w[AZURE_API_VERSION AZURE_OPENAI_API_KEY AZURE_API_BASE AZURE_DEPLOYMENT_NAME]
|
52
|
+
when :open_ai
|
53
|
+
%w[OPENAI_API_VERSION OPENAI_API_KEY]
|
54
|
+
end
|
55
|
+
required_envs.each do |required_env|
|
56
|
+
if ENV[required_env].nil? || ENV[required_env].empty?
|
57
|
+
warn("[ERROR] #{required_env} is required as a environment variable")
|
58
|
+
exit 1
|
59
|
+
end
|
60
|
+
end
|
61
|
+
logger = Ghostest::Logger.instance
|
62
|
+
logger.debug = options[:debug]
|
63
|
+
client = Ghostest::Manager.new(Ghostest::Config.load(config_path, options))
|
64
|
+
client.start_work!
|
65
|
+
rescue StandardError => e
|
66
|
+
if options[:debug]
|
67
|
+
raise e
|
68
|
+
else
|
69
|
+
warn("[ERROR] #{[e.message, e.backtrace.first].join("\n\t")}")
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
exit 0
|
data/ghostest.gemspec
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "ghostest/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "ghostest"
|
9
|
+
spec.version = Ghostest::VERSION
|
10
|
+
spec.authors = ["ryooo"]
|
11
|
+
spec.email = ["ryooo.321@gmail.com"]
|
12
|
+
|
13
|
+
spec.summary = "Test code generator by llm"
|
14
|
+
spec.description = "Output test code using LLM agents."
|
15
|
+
spec.homepage = "https://github.com/ryooo/ghostest"
|
16
|
+
# spec.required_ruby_version = ">= 3.2.1"
|
17
|
+
|
18
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
19
|
+
|
20
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
21
|
+
spec.metadata["source_code_uri"] = "https://github.com/ryooo/ghostest"
|
22
|
+
spec.metadata["changelog_uri"] = "https://github.com/ryooo/ghostest"
|
23
|
+
|
24
|
+
# Specify which files should be added to the gem when it is released.
|
25
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
26
|
+
spec.files = Dir.chdir(__dir__) do
|
27
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
28
|
+
(File.expand_path(f) == __FILE__) ||
|
29
|
+
f.start_with?(*%w[bin/ config/ spec/ features/ .git .circleci appveyor Gemfile])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
spec.bindir = "exe"
|
33
|
+
spec.executables = ['ghostest']
|
34
|
+
spec.require_paths = ["lib"]
|
35
|
+
|
36
|
+
spec.add_development_dependency "rspec"
|
37
|
+
spec.add_development_dependency "rubocop"
|
38
|
+
spec.add_dependency 'ruby-openai'
|
39
|
+
spec.add_dependency 'html2markdown'
|
40
|
+
spec.add_dependency 'addressable'
|
41
|
+
spec.add_dependency 'baran'
|
42
|
+
spec.add_dependency 'tiktoken_ruby'
|
43
|
+
spec.add_dependency 'google-apis-customsearch_v1'
|
44
|
+
spec.add_dependency 'colorize'
|
45
|
+
spec.add_dependency 'i18n'
|
46
|
+
spec.add_dependency 'indifference'
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'ghostest/attr_reader'
|
2
|
+
require 'ghostest/config'
|
3
|
+
module Ghostest
|
4
|
+
class Config::Agent
|
5
|
+
include AttrReader
|
6
|
+
attr_reader :name, :occupation, :system_prompt, :color, :role
|
7
|
+
|
8
|
+
def initialize(name, hash, global_config)
|
9
|
+
@global_config = global_config
|
10
|
+
@name = name
|
11
|
+
@role = hash[:role] || raise(ConfigError.new("Agent role is required"))
|
12
|
+
|
13
|
+
@system_prompt = hash[:system_prompt] || raise(ConfigError.new("Agent system_prompt is required"))
|
14
|
+
@color = hash[:color] || raise(ConfigError.new("Agent color is required"))
|
15
|
+
end
|
16
|
+
|
17
|
+
def role_klass
|
18
|
+
case @role.to_sym
|
19
|
+
when :test_designer
|
20
|
+
return Llm::Agents::TestDesigner
|
21
|
+
when :test_programmer
|
22
|
+
return Llm::Agents::TestProgrammer
|
23
|
+
when :reviewer
|
24
|
+
return Llm::Agents::Reviewer
|
25
|
+
else
|
26
|
+
raise ConfigError.new("Unknown agent role #{@role}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Ghostest
|
2
|
+
class Config
|
3
|
+
include AttrReader
|
4
|
+
attr_reader :llm, :debug, :max_token, :watch_files, :agents, :language
|
5
|
+
|
6
|
+
def initialize(hash, options)
|
7
|
+
@llm = options[:llm]
|
8
|
+
@debug = options[:debug]
|
9
|
+
|
10
|
+
@max_token = hash[:max_token] || 32000
|
11
|
+
@language = (hash[:language] || raise(ConfigError.new("Language is required"))).to_sym
|
12
|
+
@watch_files = hash[:watch_files] || raise(ConfigError.new("Watch files are required"))
|
13
|
+
agents = hash[:agents] || []
|
14
|
+
@agents = Hash[agents.map { |k, h| [k, Agent.new(k, h, self)] }].with_indifferent_access
|
15
|
+
raise(ConfigError.new("2 Agents are required")) if @agents.size < 2
|
16
|
+
end
|
17
|
+
|
18
|
+
def llm_klass
|
19
|
+
case @llm&.to_sym
|
20
|
+
when :azure_open_ai
|
21
|
+
return Llm::Clients::AzureOpenAi
|
22
|
+
else
|
23
|
+
raise ConfigError.new("Unknown llm #{@llm}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def language_klass
|
28
|
+
case self.language
|
29
|
+
when :ruby
|
30
|
+
return Ghostest::Languages::Ruby
|
31
|
+
else
|
32
|
+
raise ConfigError.new("Unknown language #{self.language}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class << self
|
37
|
+
def load(config_path, options)
|
38
|
+
options = options.with_indifferent_access
|
39
|
+
default_config = parse_config_file(File.expand_path('config/ghostest.yml'))
|
40
|
+
if config_path.nil?
|
41
|
+
Config.new(default_config.with_indifferent_access, options)
|
42
|
+
elsif File.exist?(config_path)
|
43
|
+
Config.new(default_config.merge(parse_config_file(config_path)).with_indifferent_access, options)
|
44
|
+
elsif (expanded = File.expand_path(config_path)) && File.exist?(expanded)
|
45
|
+
Config.new(default_config.merge(parse_config_file(expanded)).with_indifferent_access, options)
|
46
|
+
else
|
47
|
+
raise ConfigError.new("Config file #{config_path} not found")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def parse_config_file(path)
|
54
|
+
yaml = ERB.new(File.read(path)).result
|
55
|
+
|
56
|
+
YAML.safe_load(
|
57
|
+
yaml,
|
58
|
+
permitted_classes: [Symbol],
|
59
|
+
permitted_symbols: [],
|
60
|
+
aliases: true
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Ghostest
|
2
|
+
module Languages
|
3
|
+
class Ruby
|
4
|
+
def self.convert_source_path_to_test_path(source_path)
|
5
|
+
# .rbで終わるファイルパスを前提とする
|
6
|
+
"spec/#{source_path}".gsub(/\.rb$/, '_spec.rb')
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.test_condition_yml_path
|
10
|
+
"spec/ghostest_condition.yml"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.create_functions
|
14
|
+
[
|
15
|
+
Llm::Functions::ExecRspecTest.new,
|
16
|
+
Llm::Functions::GetGemFilesList.new,
|
17
|
+
]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ghostest
|
4
|
+
class Logger < ::Logger
|
5
|
+
include Singleton
|
6
|
+
def verbose
|
7
|
+
@verbose ||= false
|
8
|
+
end
|
9
|
+
|
10
|
+
def verbose=(value)
|
11
|
+
@verbose = value
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
super($stdout)
|
16
|
+
|
17
|
+
self.formatter = proc do |_severity, _datetime, _progname, msg|
|
18
|
+
"#{msg}\n"
|
19
|
+
end
|
20
|
+
|
21
|
+
self.level = Logger::INFO
|
22
|
+
end
|
23
|
+
|
24
|
+
def verbose_info(msg)
|
25
|
+
info(msg) if verbose
|
26
|
+
end
|
27
|
+
|
28
|
+
def debug=(value)
|
29
|
+
self.level = value ? Logger::DEBUG : Logger::INFO
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ghostest
|
4
|
+
class Manager
|
5
|
+
include AttrReader
|
6
|
+
attr_reader :config
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
@should_work_paths = []
|
11
|
+
@test_condition = Ghostest::TestCondition.new(@config.language_klass)
|
12
|
+
end
|
13
|
+
|
14
|
+
# skip test for this method
|
15
|
+
def start_work!
|
16
|
+
logger = Ghostest::Logger.instance
|
17
|
+
|
18
|
+
loop do
|
19
|
+
wait_for_update(@config.watch_files)
|
20
|
+
next if @should_work_paths.empty?
|
21
|
+
|
22
|
+
@should_work_paths.each do |source_path, test_path|
|
23
|
+
agents = @config.agents.map do |name, agent_config|
|
24
|
+
agent_config.role_klass.new(name, @config, logger)
|
25
|
+
end
|
26
|
+
|
27
|
+
assignee = agents.first
|
28
|
+
switch_assignee_function = Llm::Functions::SwitchAssignee.new(assignee, agents)
|
29
|
+
i = 0
|
30
|
+
while i < 10
|
31
|
+
i += 1
|
32
|
+
assignee.work(source_path:, test_path:, switch_assignee_function:)
|
33
|
+
break if assignee.respond_to?('lgtm?') && assignee.lgtm?
|
34
|
+
assignee = switch_assignee_function.assignee
|
35
|
+
end
|
36
|
+
if assignee.respond_to?('lgtm?') && assignee.lgtm?
|
37
|
+
@test_condition.save_as_updated!(source_path)
|
38
|
+
else
|
39
|
+
raise Ghostest::Error, "Workers couldn't finish the work. "
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# skip test for this method
|
46
|
+
def wait_for_update(watch_files)
|
47
|
+
loop do
|
48
|
+
sleep(3)
|
49
|
+
should_work_paths = []
|
50
|
+
watch_files.each do |watch_file|
|
51
|
+
file_paths = Dir.glob(watch_file)
|
52
|
+
|
53
|
+
file_paths.each do |source_path|
|
54
|
+
if @test_condition.should_update_test?(source_path)
|
55
|
+
test_path = @config.language_klass.convert_source_path_to_test_path(source_path)
|
56
|
+
if File.exist?(test_path)
|
57
|
+
should_work_paths << [source_path, test_path]
|
58
|
+
else
|
59
|
+
should_work_paths << [source_path, nil]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
unless should_work_paths.empty?
|
66
|
+
@should_work_paths = should_work_paths
|
67
|
+
break
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|