tng 0.2.1
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/Gemfile +15 -0
- data/LICENSE.md +32 -0
- data/README.md +413 -0
- data/Rakefile +124 -0
- data/bin/load_dev +22 -0
- data/bin/tng +888 -0
- data/binaries/tng.bundle +0 -0
- data/binaries/tng.so +0 -0
- data/lib/generators/tng/install_generator.rb +236 -0
- data/lib/tng/analyzers/controller.rb +114 -0
- data/lib/tng/analyzers/model.rb +131 -0
- data/lib/tng/analyzers/other.rb +277 -0
- data/lib/tng/analyzers/service.rb +150 -0
- data/lib/tng/api/http_client.rb +100 -0
- data/lib/tng/railtie.rb +11 -0
- data/lib/tng/services/direct_generation.rb +320 -0
- data/lib/tng/services/extract_methods.rb +39 -0
- data/lib/tng/services/test_generator.rb +287 -0
- data/lib/tng/services/testng.rb +100 -0
- data/lib/tng/services/user_app_config.rb +76 -0
- data/lib/tng/ui/about_display.rb +66 -0
- data/lib/tng/ui/authentication_warning_display.rb +172 -0
- data/lib/tng/ui/configuration_display.rb +52 -0
- data/lib/tng/ui/controller_test_flow_display.rb +79 -0
- data/lib/tng/ui/display_banner.rb +44 -0
- data/lib/tng/ui/goodbye_display.rb +41 -0
- data/lib/tng/ui/model_test_flow_display.rb +80 -0
- data/lib/tng/ui/other_test_flow_display.rb +78 -0
- data/lib/tng/ui/post_install_box.rb +80 -0
- data/lib/tng/ui/service_test_flow_display.rb +78 -0
- data/lib/tng/ui/show_help.rb +78 -0
- data/lib/tng/ui/system_status_display.rb +128 -0
- data/lib/tng/ui/theme.rb +258 -0
- data/lib/tng/ui/user_stats_display.rb +160 -0
- data/lib/tng/utils.rb +325 -0
- data/lib/tng/version.rb +5 -0
- data/lib/tng.rb +308 -0
- data/tng.gemspec +56 -0
- metadata +293 -0
@@ -0,0 +1,320 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "extract_methods"
|
4
|
+
|
5
|
+
module Tng
|
6
|
+
module Services
|
7
|
+
class DirectGeneration
|
8
|
+
include ExtractMethods
|
9
|
+
|
10
|
+
def initialize(pastel, testng, http_client, params, show_post_generation_menu_proc)
|
11
|
+
@pastel = pastel
|
12
|
+
@testng = testng
|
13
|
+
@http_client = http_client
|
14
|
+
@params = params
|
15
|
+
@show_post_generation_menu = show_post_generation_menu_proc
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
type = normalize_type(@params[:type])
|
20
|
+
file_name = @params[:file]
|
21
|
+
method_name = @params[:method]
|
22
|
+
|
23
|
+
unless type && file_name && method_name
|
24
|
+
puts @pastel.red("❌ All parameters are required for direct generation: --type, --file, --method")
|
25
|
+
puts @pastel.yellow("Usage: bundle exec tng --type=controller --file=users --method=index")
|
26
|
+
puts @pastel.yellow("Shortcuts: bundle exec tng -t=c -f=users -m=index")
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
unless %w[controller model service other].include?(type)
|
31
|
+
puts @pastel.red("❌ Invalid type: #{@params[:type]}")
|
32
|
+
puts @pastel.yellow("Supported types: controller, model, service, other")
|
33
|
+
return
|
34
|
+
end
|
35
|
+
|
36
|
+
case type
|
37
|
+
when "controller"
|
38
|
+
run_controller_method_generation(file_name, method_name)
|
39
|
+
when "model"
|
40
|
+
run_model_method_generation(file_name, method_name)
|
41
|
+
when "service"
|
42
|
+
run_service_method_generation(file_name, method_name)
|
43
|
+
when "other"
|
44
|
+
run_other_method_generation(file_name, method_name)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def normalize_type(type_param)
|
49
|
+
case type_param&.downcase
|
50
|
+
when "c", "controller"
|
51
|
+
"controller"
|
52
|
+
when "m", "mo", "model"
|
53
|
+
"model"
|
54
|
+
when "s", "se", "service"
|
55
|
+
"service"
|
56
|
+
when "o", "other"
|
57
|
+
"other"
|
58
|
+
else
|
59
|
+
type_param&.downcase
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def run_controller_method_generation(file_name, method_name)
|
64
|
+
controllers = Tng::Analyzers::Controller.files_in_dir("app/controllers").map do |file|
|
65
|
+
relative_path = file[:path].gsub(%r{^.*app/controllers/}, "").gsub(".rb", "")
|
66
|
+
namespaced_name = relative_path.split("/").map(&:camelize).join("::")
|
67
|
+
|
68
|
+
{
|
69
|
+
name: namespaced_name,
|
70
|
+
path: file[:path]
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
controller = find_matching_controller(controllers, file_name)
|
75
|
+
|
76
|
+
unless controller
|
77
|
+
puts @pastel.red("❌ Controller not found: #{file_name}")
|
78
|
+
puts @pastel.yellow("Available controllers:")
|
79
|
+
controllers.first(5).each do |ctrl|
|
80
|
+
puts @pastel.dim(" - #{ctrl[:name]}")
|
81
|
+
end
|
82
|
+
puts @pastel.dim(" ... and #{controllers.length - 5} more") if controllers.length > 5
|
83
|
+
return
|
84
|
+
end
|
85
|
+
|
86
|
+
methods = extract_controller_methods(controller)
|
87
|
+
|
88
|
+
if methods.empty?
|
89
|
+
puts @pastel.yellow("⚠️ No methods found in #{controller[:name]}")
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
method_info = methods.find { |method| method[:name].downcase == method_name.downcase }
|
94
|
+
|
95
|
+
unless method_info
|
96
|
+
puts @pastel.red("❌ Method '#{method_name}' not found in #{controller[:name]}")
|
97
|
+
puts @pastel.yellow("Available methods:")
|
98
|
+
methods.first(10).each do |method|
|
99
|
+
puts @pastel.dim(" - #{method[:name]}")
|
100
|
+
end
|
101
|
+
puts @pastel.dim(" ... and #{methods.length - 10} more") if methods.length > 10
|
102
|
+
return
|
103
|
+
end
|
104
|
+
|
105
|
+
puts @pastel.bright_white("🎯 Generating test for #{controller[:name]}##{method_info[:name]}...")
|
106
|
+
|
107
|
+
result = Tng::Services::TestGenerator.new(@http_client).run_for_controller_method(controller, method_info)
|
108
|
+
|
109
|
+
if result && result[:file_path]
|
110
|
+
@show_post_generation_menu.call(result)
|
111
|
+
else
|
112
|
+
puts @pastel.red("❌ Failed to generate test")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def find_matching_controller(controllers, file_name)
|
117
|
+
controllers.find do |controller|
|
118
|
+
controller[:name].downcase == file_name.downcase ||
|
119
|
+
controller[:name].split("::").last.downcase == file_name.downcase ||
|
120
|
+
File.basename(controller[:path], ".rb").downcase == file_name.downcase ||
|
121
|
+
File.basename(controller[:path], ".rb").gsub("_controller", "").downcase == file_name.downcase ||
|
122
|
+
File.basename(controller[:path], ".rb").downcase == "#{file_name}_controller".downcase
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def run_model_method_generation(file_name, method_name)
|
127
|
+
models = Tng::Analyzers::Model.files_in_dir("app/models").map do |file|
|
128
|
+
relative_path = file[:path].gsub(%r{^.*app/models/}, "").gsub(".rb", "")
|
129
|
+
namespaced_name = relative_path.split("/").map(&:camelize).join("::")
|
130
|
+
|
131
|
+
{
|
132
|
+
name: namespaced_name,
|
133
|
+
path: file[:path]
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
model = find_matching_model(models, file_name)
|
138
|
+
|
139
|
+
unless model
|
140
|
+
puts @pastel.red("❌ Model not found: #{file_name}")
|
141
|
+
puts @pastel.yellow("Available models:")
|
142
|
+
models.first(5).each do |mdl|
|
143
|
+
puts @pastel.dim(" - #{mdl[:name]}")
|
144
|
+
end
|
145
|
+
puts @pastel.dim(" ... and #{models.length - 5} more") if models.length > 5
|
146
|
+
return
|
147
|
+
end
|
148
|
+
|
149
|
+
methods = extract_model_methods(model)
|
150
|
+
|
151
|
+
if methods.empty?
|
152
|
+
puts @pastel.yellow("⚠️ No methods found in #{model[:name]}")
|
153
|
+
return
|
154
|
+
end
|
155
|
+
|
156
|
+
method_info = methods.find { |method| method[:name].downcase == method_name.downcase }
|
157
|
+
|
158
|
+
unless method_info
|
159
|
+
puts @pastel.red("❌ Method '#{method_name}' not found in #{model[:name]}")
|
160
|
+
puts @pastel.yellow("Available methods:")
|
161
|
+
methods.first(10).each do |method|
|
162
|
+
puts @pastel.dim(" - #{method[:name]}")
|
163
|
+
end
|
164
|
+
puts @pastel.dim(" ... and #{methods.length - 10} more") if methods.length > 10
|
165
|
+
return
|
166
|
+
end
|
167
|
+
|
168
|
+
puts @pastel.bright_white("🎯 Generating test for #{model[:name]}##{method_info[:name]}...")
|
169
|
+
|
170
|
+
result = Tng::Services::TestGenerator.new(@http_client).run_for_model_method(model, method_info)
|
171
|
+
|
172
|
+
if result && result[:file_path]
|
173
|
+
@show_post_generation_menu.call(result)
|
174
|
+
else
|
175
|
+
puts @pastel.red("❌ Failed to generate test")
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def find_matching_model(models, file_name)
|
180
|
+
models.find do |model|
|
181
|
+
model[:name].downcase == file_name.downcase ||
|
182
|
+
model[:name].split("::").last.downcase == file_name.downcase ||
|
183
|
+
File.basename(model[:path], ".rb").downcase == file_name.downcase
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def run_service_method_generation(file_name, method_name)
|
188
|
+
services = Tng::Analyzers::Service.files_in_dir.map do |file|
|
189
|
+
relative_path = file[:path].gsub(%r{^.*app/services?/}, "").gsub(".rb", "")
|
190
|
+
namespaced_name = relative_path.split("/").map(&:camelize).join("::")
|
191
|
+
|
192
|
+
{
|
193
|
+
name: namespaced_name,
|
194
|
+
path: file[:path]
|
195
|
+
}
|
196
|
+
end
|
197
|
+
|
198
|
+
service = find_matching_service(services, file_name)
|
199
|
+
|
200
|
+
unless service
|
201
|
+
puts @pastel.red("❌ Service not found: #{file_name}")
|
202
|
+
puts @pastel.yellow("Available services:")
|
203
|
+
services.first(5).each do |svc|
|
204
|
+
puts @pastel.dim(" - #{svc[:name]}")
|
205
|
+
end
|
206
|
+
puts @pastel.dim(" ... and #{services.length - 5} more") if services.length > 5
|
207
|
+
return
|
208
|
+
end
|
209
|
+
|
210
|
+
methods = extract_service_methods(service)
|
211
|
+
|
212
|
+
if methods.empty?
|
213
|
+
puts @pastel.yellow("⚠️ No methods found in #{service[:name]}")
|
214
|
+
return
|
215
|
+
end
|
216
|
+
|
217
|
+
method_info = methods.find { |method| method[:name].downcase == method_name.downcase }
|
218
|
+
|
219
|
+
unless method_info
|
220
|
+
puts @pastel.red("❌ Method '#{method_name}' not found in #{service[:name]}")
|
221
|
+
puts @pastel.yellow("Available methods:")
|
222
|
+
methods.first(10).each do |method|
|
223
|
+
puts @pastel.dim(" - #{method[:name]}")
|
224
|
+
end
|
225
|
+
puts @pastel.dim(" ... and #{methods.length - 10} more") if methods.length > 10
|
226
|
+
return
|
227
|
+
end
|
228
|
+
|
229
|
+
puts @pastel.bright_white("🎯 Generating test for #{service[:name]}##{method_info[:name]}...")
|
230
|
+
|
231
|
+
result = Tng::Services::TestGenerator.new(@http_client).run_for_service_method(service, method_info)
|
232
|
+
|
233
|
+
if result && result[:file_path]
|
234
|
+
@show_post_generation_menu.call(result)
|
235
|
+
else
|
236
|
+
puts @pastel.red("❌ Failed to generate test")
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def find_matching_service(services, file_name)
|
241
|
+
services.find do |service|
|
242
|
+
service[:name].downcase == file_name.downcase ||
|
243
|
+
service[:name].split("::").last.downcase == file_name.downcase ||
|
244
|
+
File.basename(service[:path], ".rb").downcase == file_name.downcase
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def run_other_method_generation(file_name, method_name)
|
249
|
+
other_files = Tng::Analyzers::Other.files_in_dir.map do |file|
|
250
|
+
relative_path = file[:relative_path].gsub(".rb", "")
|
251
|
+
namespaced_name = relative_path.split("/").map(&:camelize).join("::")
|
252
|
+
|
253
|
+
{
|
254
|
+
name: namespaced_name,
|
255
|
+
path: file[:path],
|
256
|
+
type: file[:type]
|
257
|
+
}
|
258
|
+
end
|
259
|
+
|
260
|
+
other_file = find_matching_other_file(other_files, file_name)
|
261
|
+
|
262
|
+
unless other_file
|
263
|
+
puts @pastel.red("❌ Other file not found: #{file_name}")
|
264
|
+
puts @pastel.yellow("Available other files:")
|
265
|
+
other_files.first(5).each do |file|
|
266
|
+
puts @pastel.dim(" - #{file[:name]}")
|
267
|
+
end
|
268
|
+
puts @pastel.dim(" ... and #{other_files.length - 5} more") if other_files.length > 5
|
269
|
+
return
|
270
|
+
end
|
271
|
+
|
272
|
+
methods = extract_other_methods(other_file)
|
273
|
+
|
274
|
+
if methods.empty?
|
275
|
+
puts @pastel.yellow("⚠️ No methods found in #{other_file[:name]}")
|
276
|
+
return
|
277
|
+
end
|
278
|
+
|
279
|
+
method_info = methods.find { |method| method[:name].downcase == method_name.downcase }
|
280
|
+
|
281
|
+
unless method_info
|
282
|
+
puts @pastel.red("❌ Method '#{method_name}' not found in #{other_file[:name]}")
|
283
|
+
puts @pastel.yellow("Available methods:")
|
284
|
+
methods.first(10).each do |method|
|
285
|
+
puts @pastel.dim(" - #{method[:name]}")
|
286
|
+
end
|
287
|
+
puts @pastel.dim(" ... and #{methods.length - 10} more") if methods.length > 10
|
288
|
+
return
|
289
|
+
end
|
290
|
+
|
291
|
+
puts @pastel.bright_white("🎯 Generating test for #{other_file[:name]}##{method_info[:name]}...")
|
292
|
+
|
293
|
+
result = Tng::Services::TestGenerator.new(@http_client).run_for_other_method(other_file, method_info)
|
294
|
+
|
295
|
+
if result && result[:file_path]
|
296
|
+
@show_post_generation_menu.call(result)
|
297
|
+
else
|
298
|
+
puts @pastel.red("❌ Failed to generate test")
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def find_matching_other_file(other_files, file_name)
|
303
|
+
other_files.find do |file|
|
304
|
+
file[:name].downcase == file_name.downcase ||
|
305
|
+
file[:name].split("::").last.downcase == file_name.downcase ||
|
306
|
+
File.basename(file[:path], ".rb").downcase == file_name.downcase ||
|
307
|
+
normalize_other_file_path(file[:path]).downcase == file_name.downcase
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def normalize_other_file_path(file_path)
|
312
|
+
Tng::Analyzers::Other::OTHER_DIRECTORIES.each do |dir|
|
313
|
+
pattern = %r{^.*/#{Regexp.escape(dir)}/}
|
314
|
+
return file_path.gsub(pattern, "").gsub(".rb", "") if file_path.match?(pattern)
|
315
|
+
end
|
316
|
+
file_path # fallback if no match
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tng
|
4
|
+
module Services
|
5
|
+
module ExtractMethods
|
6
|
+
def extract_controller_methods(controller)
|
7
|
+
methods = Tng::Analyzers::Controller.methods_for_controller(controller[:name])
|
8
|
+
Array(methods)
|
9
|
+
rescue StandardError => e
|
10
|
+
puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing controller: #{e.message}", Tng::UI::Theme.color(:error)))
|
11
|
+
[]
|
12
|
+
end
|
13
|
+
|
14
|
+
def extract_model_methods(model)
|
15
|
+
methods = Tng::Analyzers::Model.methods_for_model(model[:name])
|
16
|
+
Array(methods)
|
17
|
+
rescue StandardError => e
|
18
|
+
puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing model: #{e.message}", Tng::UI::Theme.color(:error)))
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
|
22
|
+
def extract_service_methods(service)
|
23
|
+
methods = Tng::Analyzers::Service.methods_for_service(service[:name])
|
24
|
+
Array(methods)
|
25
|
+
rescue StandardError => e
|
26
|
+
puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing service: #{e.message}", Tng::UI::Theme.color(:error)))
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
|
30
|
+
def extract_other_methods(other_file)
|
31
|
+
methods = Tng::Analyzers::Other.methods_for_other(other_file[:name], other_file[:path])
|
32
|
+
Array(methods)
|
33
|
+
rescue StandardError => e
|
34
|
+
puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing file: #{e.message}", Tng::UI::Theme.color(:error)))
|
35
|
+
[]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,287 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zlib"
|
4
|
+
require "tng/utils"
|
5
|
+
require "tng/services/user_app_config"
|
6
|
+
require "tng/ui/theme"
|
7
|
+
require "pastel"
|
8
|
+
|
9
|
+
module Tng
|
10
|
+
module Services
|
11
|
+
class TestGenerator
|
12
|
+
GENERATE_TESTS_PATH = "cli/tng_rails/contents/generate_tests"
|
13
|
+
CONTENT_RESPONSES_PATH = "cli/tng_rails/content_responses"
|
14
|
+
POLL_INTERVAL_SECONDS = 5 # Poll every 5 seconds
|
15
|
+
MAX_POLL_DURATION_SECONDS = 360 # 6 minutes total
|
16
|
+
|
17
|
+
def initialize(http_client)
|
18
|
+
@http_client = http_client
|
19
|
+
@machine_info = Tng.machine_info
|
20
|
+
@pastel = Pastel.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_for_controller_method(controller, method_info)
|
24
|
+
start_time = Time.now
|
25
|
+
|
26
|
+
response = Tng.send_request_for_controller(
|
27
|
+
controller[:name],
|
28
|
+
controller[:path],
|
29
|
+
method_info[:name],
|
30
|
+
Tng::Utils.fixture_content,
|
31
|
+
Tng::Services::UserAppConfig.config_with_source,
|
32
|
+
Tng::Services::UserAppConfig.base_url,
|
33
|
+
Tng::Services::UserAppConfig.api_key
|
34
|
+
)
|
35
|
+
|
36
|
+
job_data = JSON.parse(response.body)
|
37
|
+
job_id = job_data["job_id"]
|
38
|
+
|
39
|
+
return unless job_id
|
40
|
+
|
41
|
+
result = poll_for_completion(job_id)
|
42
|
+
|
43
|
+
return unless result
|
44
|
+
|
45
|
+
end_time = Time.now
|
46
|
+
generation_time = end_time - start_time
|
47
|
+
|
48
|
+
file_result = Tng::Utils.save_test_file(result.to_json)
|
49
|
+
return unless file_result
|
50
|
+
|
51
|
+
file_result.merge(generation_time: generation_time)
|
52
|
+
end
|
53
|
+
|
54
|
+
def run_for_model_method(model, method_info)
|
55
|
+
start_time = Time.now
|
56
|
+
|
57
|
+
response = Tng.send_request_for_model(
|
58
|
+
model[:name],
|
59
|
+
model[:path],
|
60
|
+
method_info[:name],
|
61
|
+
Tng::Utils.fixture_content,
|
62
|
+
Tng::Services::UserAppConfig.config_with_source,
|
63
|
+
Tng::Services::UserAppConfig.base_url,
|
64
|
+
Tng::Services::UserAppConfig.api_key
|
65
|
+
)
|
66
|
+
|
67
|
+
job_data = JSON.parse(response.body)
|
68
|
+
job_id = job_data["job_id"]
|
69
|
+
|
70
|
+
return unless job_id
|
71
|
+
|
72
|
+
result = poll_for_completion(job_id)
|
73
|
+
|
74
|
+
return unless result
|
75
|
+
|
76
|
+
end_time = Time.now
|
77
|
+
generation_time = end_time - start_time
|
78
|
+
|
79
|
+
file_result = Tng::Utils.save_test_file(result.to_json)
|
80
|
+
return unless file_result
|
81
|
+
|
82
|
+
file_result.merge(generation_time: generation_time)
|
83
|
+
end
|
84
|
+
|
85
|
+
def run_for_service_method(service, method_info)
|
86
|
+
start_time = Time.now
|
87
|
+
|
88
|
+
response = Tng.send_request_for_service(
|
89
|
+
service[:name],
|
90
|
+
service[:path],
|
91
|
+
method_info[:name],
|
92
|
+
Tng::Utils.fixture_content,
|
93
|
+
Tng::Services::UserAppConfig.config_with_source,
|
94
|
+
Tng::Services::UserAppConfig.base_url,
|
95
|
+
Tng::Services::UserAppConfig.api_key
|
96
|
+
)
|
97
|
+
|
98
|
+
job_data = JSON.parse(response.body)
|
99
|
+
job_id = job_data["job_id"]
|
100
|
+
|
101
|
+
return unless job_id
|
102
|
+
|
103
|
+
result = poll_for_completion(job_id)
|
104
|
+
|
105
|
+
return unless result
|
106
|
+
|
107
|
+
end_time = Time.now
|
108
|
+
generation_time = end_time - start_time
|
109
|
+
|
110
|
+
file_result = Tng::Utils.save_test_file(result.to_json)
|
111
|
+
return unless file_result
|
112
|
+
|
113
|
+
file_result.merge(generation_time: generation_time)
|
114
|
+
end
|
115
|
+
|
116
|
+
def run_for_other_method(other_file, method_info)
|
117
|
+
start_time = Time.now
|
118
|
+
|
119
|
+
response = Tng.send_request_for_other(
|
120
|
+
other_file[:name] || File.basename(other_file[:path], ".rb"),
|
121
|
+
other_file[:path],
|
122
|
+
method_info[:name],
|
123
|
+
Tng::Utils.fixture_content,
|
124
|
+
Tng::Services::UserAppConfig.config_with_source,
|
125
|
+
Tng::Services::UserAppConfig.base_url,
|
126
|
+
Tng::Services::UserAppConfig.api_key
|
127
|
+
)
|
128
|
+
|
129
|
+
job_data = JSON.parse(response.body)
|
130
|
+
job_id = job_data["job_id"]
|
131
|
+
|
132
|
+
return unless job_id
|
133
|
+
|
134
|
+
result = poll_for_completion(job_id)
|
135
|
+
|
136
|
+
return unless result
|
137
|
+
|
138
|
+
end_time = Time.now
|
139
|
+
generation_time = end_time - start_time
|
140
|
+
|
141
|
+
file_result = Tng::Utils.save_test_file(result.to_json)
|
142
|
+
return unless file_result
|
143
|
+
|
144
|
+
file_result.merge(generation_time: generation_time)
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def send_request_and_save_test(payload)
|
150
|
+
job_id = submit_async_job(payload)
|
151
|
+
return unless job_id
|
152
|
+
|
153
|
+
result = poll_for_completion(job_id)
|
154
|
+
return unless result
|
155
|
+
|
156
|
+
Tng::Utils.save_test_file(result.to_json)
|
157
|
+
rescue StandardError => e
|
158
|
+
debug_log("Async request failed: #{e.message}") if debug_enabled?
|
159
|
+
puts "❌ Failed to generate test: #{e.message}"
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
|
163
|
+
def submit_async_job(payload)
|
164
|
+
marshaled = Marshal.dump(payload)
|
165
|
+
compressed = Zlib::Deflate.deflate(marshaled)
|
166
|
+
response = @http_client.post_binary(GENERATE_TESTS_PATH, compressed)
|
167
|
+
|
168
|
+
if response.is_a?(HTTPX::ErrorResponse)
|
169
|
+
debug_log("Processing test failed") if debug_enabled?
|
170
|
+
puts "❌ Failed to submit test generation job: #{response.error&.message}"
|
171
|
+
return
|
172
|
+
end
|
173
|
+
|
174
|
+
job_data = JSON.parse(response.body)
|
175
|
+
job_id = job_data["job_id"]
|
176
|
+
|
177
|
+
debug_log("Processing test with id: #{job_id}") if debug_enabled?
|
178
|
+
|
179
|
+
job_id
|
180
|
+
rescue JSON::ParserError => e
|
181
|
+
debug_log("Failed to parse API response: #{e.message}") if debug_enabled?
|
182
|
+
puts "❌ Failed to parse API response. Please retry."
|
183
|
+
end
|
184
|
+
|
185
|
+
def poll_for_completion(job_id)
|
186
|
+
start_time = Time.current
|
187
|
+
|
188
|
+
rocket_icon = Tng::UI::Theme.icon(:rocket)
|
189
|
+
success_color = Tng::UI::Theme.color(:success)
|
190
|
+
primary_color = Tng::UI::Theme.color(:primary)
|
191
|
+
muted_color = Tng::UI::Theme.color(:muted)
|
192
|
+
|
193
|
+
complete_char = @pastel.decorate("▓", success_color)
|
194
|
+
incomplete_char = @pastel.decorate("░", muted_color)
|
195
|
+
head_char = @pastel.decorate("▶", success_color)
|
196
|
+
|
197
|
+
progress_bar = TTY::ProgressBar.new(
|
198
|
+
"#{rocket_icon} #{@pastel.decorate("Generating tests",
|
199
|
+
primary_color)} #{@pastel.decorate("[:bar]",
|
200
|
+
:white)} #{@pastel.decorate(":status",
|
201
|
+
muted_color)} #{@pastel.decorate(
|
202
|
+
"(:elapsed)", muted_color
|
203
|
+
)}",
|
204
|
+
total: nil,
|
205
|
+
complete: complete_char,
|
206
|
+
incomplete: incomplete_char,
|
207
|
+
head: head_char,
|
208
|
+
width: 40,
|
209
|
+
clear: true,
|
210
|
+
frequency: 10, # Update 10 times per second for smooth animation
|
211
|
+
interval: 1 # Show elapsed time updates every second
|
212
|
+
)
|
213
|
+
|
214
|
+
loop do
|
215
|
+
seconds_elapsed = (Time.current - start_time).to_i
|
216
|
+
|
217
|
+
if seconds_elapsed > MAX_POLL_DURATION_SECONDS
|
218
|
+
progress_bar.finish
|
219
|
+
puts "❌ Test generation timed out after #{MAX_POLL_DURATION_SECONDS} seconds (6 minutes)"
|
220
|
+
return
|
221
|
+
end
|
222
|
+
|
223
|
+
status_text = case seconds_elapsed
|
224
|
+
when 0..15 then "initializing..."
|
225
|
+
when 16..45 then "analyzing code structure..."
|
226
|
+
when 46..90 then "generating test cases..."
|
227
|
+
when 91..150 then "optimizing test logic..."
|
228
|
+
when 151..210 then "refining assertions..."
|
229
|
+
when 211..270 then "formatting output..."
|
230
|
+
when 271..330 then "finalizing tests..."
|
231
|
+
else "completing generation..."
|
232
|
+
end
|
233
|
+
progress_bar.advance(1, status: status_text)
|
234
|
+
|
235
|
+
sleep(POLL_INTERVAL_SECONDS)
|
236
|
+
|
237
|
+
status_response = @http_client.get("#{CONTENT_RESPONSES_PATH}/#{job_id}")
|
238
|
+
|
239
|
+
if status_response.is_a?(HTTPX::ErrorResponse)
|
240
|
+
debug_log("Status check failed: #{status_response.error&.message}") if debug_enabled?
|
241
|
+
next # Continue polling on network errors
|
242
|
+
end
|
243
|
+
|
244
|
+
begin
|
245
|
+
status_data = JSON.parse(status_response.body.to_s)
|
246
|
+
status = status_data["status"]
|
247
|
+
|
248
|
+
case status
|
249
|
+
when "completed"
|
250
|
+
debug_log("Test generation completed!") if debug_enabled?
|
251
|
+
progress_bar.finish
|
252
|
+
success_icon = Tng::UI::Theme.icon(:success)
|
253
|
+
success_color = Tng::UI::Theme.color(:success)
|
254
|
+
puts "#{success_icon} #{@pastel.decorate("Test generation completed!", success_color)}"
|
255
|
+
return status_data["result"]
|
256
|
+
when "failed"
|
257
|
+
debug_log("Test generation failed: #{status_data["error"]}") if debug_enabled?
|
258
|
+
progress_bar.finish
|
259
|
+
error_icon = Tng::UI::Theme.icon(:error)
|
260
|
+
error_color = Tng::UI::Theme.color(:error)
|
261
|
+
puts "#{error_icon} #{@pastel.decorate("Test generation failed:",
|
262
|
+
error_color)} #{status_data["error"] || "Unknown error"}"
|
263
|
+
return
|
264
|
+
when "pending", "processing"
|
265
|
+
# Progress bar updates smoothly above
|
266
|
+
next
|
267
|
+
else
|
268
|
+
debug_log("Unknown test generation status: #{status}") if debug_enabled?
|
269
|
+
next
|
270
|
+
end
|
271
|
+
rescue JSON::ParserError => e
|
272
|
+
debug_log("Failed to parse response status: #{e.message}") if debug_enabled?
|
273
|
+
next
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def debug_enabled?
|
279
|
+
ENV["DEBUG"] == "1"
|
280
|
+
end
|
281
|
+
|
282
|
+
def debug_log(message)
|
283
|
+
puts "-> DEBUG [TestGenerator]: #{message}"
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|