ghostest 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.idea/misc.xml +4 -0
  3. data/.idea/modules.xml +8 -0
  4. data/.idea/vcs.xml +6 -0
  5. data/.rubocop.yml +236 -0
  6. data/.ruby-version +1 -0
  7. data/README.md +31 -0
  8. data/Rakefile +4 -0
  9. data/exe/ghostest +74 -0
  10. data/ghostest.gemspec +48 -0
  11. data/lib/ghostest/attr_reader.rb +11 -0
  12. data/lib/ghostest/config/agent.rb +30 -0
  13. data/lib/ghostest/config.rb +65 -0
  14. data/lib/ghostest/config_error.rb +5 -0
  15. data/lib/ghostest/error.rb +5 -0
  16. data/lib/ghostest/languages/ruby.rb +21 -0
  17. data/lib/ghostest/logger.rb +32 -0
  18. data/lib/ghostest/manager.rb +72 -0
  19. data/lib/ghostest/test_condition.rb +24 -0
  20. data/lib/ghostest/version.rb +3 -0
  21. data/lib/ghostest.rb +58 -0
  22. data/lib/google_custom_search.rb +30 -0
  23. data/lib/i18n_translator.rb +66 -0
  24. data/lib/initializers/i18n.rb +9 -0
  25. data/lib/llm/agents/base.rb +31 -0
  26. data/lib/llm/agents/reviewer.rb +50 -0
  27. data/lib/llm/agents/test_designer.rb +43 -0
  28. data/lib/llm/agents/test_programmer.rb +45 -0
  29. data/lib/llm/clients/azure_open_ai.rb +15 -0
  30. data/lib/llm/clients/base.rb +88 -0
  31. data/lib/llm/clients/open_ai.rb +14 -0
  32. data/lib/llm/functions/add_to_memory.rb +41 -0
  33. data/lib/llm/functions/base.rb +13 -0
  34. data/lib/llm/functions/exec_rspec_test.rb +39 -0
  35. data/lib/llm/functions/fix_one_rspec_test.rb +55 -0
  36. data/lib/llm/functions/get_files_list.rb +29 -0
  37. data/lib/llm/functions/get_gem_files_list.rb +43 -0
  38. data/lib/llm/functions/make_new_file.rb +43 -0
  39. data/lib/llm/functions/overwrite_file.rb +42 -0
  40. data/lib/llm/functions/read_file.rb +43 -0
  41. data/lib/llm/functions/record_lgtm.rb +48 -0
  42. data/lib/llm/functions/report_bug.rb +34 -0
  43. data/lib/llm/functions/switch_assignee.rb +74 -0
  44. data/lib/llm/message_container.rb +63 -0
  45. data/sig/ghostest.rbs +4 -0
  46. 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
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager" version="2" project-jdk-name="rbenv: 3.1.0" project-jdk-type="RUBY_SDK" />
4
+ </project>
data/.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/ghostest.iml" filepath="$PROJECT_DIR$/.idea/ghostest.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
data/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
+ </component>
6
+ </project>
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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
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,11 @@
1
+ module AttrReader
2
+ private
3
+ def attr_reader(*args)
4
+ args.each do |name|
5
+ define_method name do
6
+ return instance_variable_get "@#{name}"
7
+ end
8
+ end
9
+ nil
10
+ end
11
+ 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,5 @@
1
+ module Ghostest
2
+ class ConfigError < StandardError
3
+
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Ghostest
2
+ class Error < StandardError
3
+
4
+ end
5
+ 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