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
data/bin/tng
ADDED
@@ -0,0 +1,888 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
$0 = "tng"
|
5
|
+
|
6
|
+
lib_path = File.expand_path("../lib", __dir__)
|
7
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
8
|
+
|
9
|
+
require "tng"
|
10
|
+
require "tng/services/direct_generation"
|
11
|
+
require "tng/services/extract_methods"
|
12
|
+
|
13
|
+
require "tty-prompt"
|
14
|
+
require "tty-spinner"
|
15
|
+
require "tty-box"
|
16
|
+
require "tty-table"
|
17
|
+
require "tty-progressbar"
|
18
|
+
require "tty-screen"
|
19
|
+
require "tty-option"
|
20
|
+
require "pastel"
|
21
|
+
require "tng/ui/show_help"
|
22
|
+
require "tng/ui/post_install_box"
|
23
|
+
require "tng/ui/theme"
|
24
|
+
require "tng/ui/system_status_display"
|
25
|
+
require "tng/ui/configuration_display"
|
26
|
+
require "tng/ui/authentication_warning_display"
|
27
|
+
require "tng/ui/display_banner"
|
28
|
+
require "tng/ui/user_stats_display"
|
29
|
+
require "tng/ui/about_display"
|
30
|
+
require "tng/services/testng"
|
31
|
+
require "tng/ui/goodbye_display"
|
32
|
+
require "tng/api/http_client"
|
33
|
+
require "tng/ui/controller_test_flow_display"
|
34
|
+
require "tng/ui/model_test_flow_display"
|
35
|
+
require "tng/ui/service_test_flow_display"
|
36
|
+
require "tng/ui/other_test_flow_display"
|
37
|
+
require "tng/analyzers/controller"
|
38
|
+
require "tng/analyzers/model"
|
39
|
+
require "tng/analyzers/service"
|
40
|
+
require "tng/analyzers/other"
|
41
|
+
require "tng/services/test_generator"
|
42
|
+
require "tng/services/user_app_config"
|
43
|
+
require "tng/utils"
|
44
|
+
|
45
|
+
class CLI
|
46
|
+
include Tng::Services::ExtractMethods
|
47
|
+
include TTY::Option
|
48
|
+
include Tng::Utils
|
49
|
+
|
50
|
+
usage do
|
51
|
+
program "tng"
|
52
|
+
desc "LLM-Powered Rails Test Generator"
|
53
|
+
example "Interactive mode (method-specific test generation):", "bundle exec tng"
|
54
|
+
example " • Browse and search controllers/models/services", ""
|
55
|
+
example " • Select specific methods from filtered lists", ""
|
56
|
+
example " • Generate tests for individual methods", ""
|
57
|
+
end
|
58
|
+
|
59
|
+
option :type do
|
60
|
+
short "-t"
|
61
|
+
long "--type=TYPE"
|
62
|
+
desc "Test type (controller, model, service, other)"
|
63
|
+
permit %w[controller model service other c m mo s se o]
|
64
|
+
end
|
65
|
+
|
66
|
+
option :file do
|
67
|
+
short "-f"
|
68
|
+
long "--file=FILE"
|
69
|
+
desc "File name (without .rb extension)"
|
70
|
+
end
|
71
|
+
|
72
|
+
option :method do
|
73
|
+
short "-m"
|
74
|
+
long "--method=METHOD"
|
75
|
+
desc "Method name (for per-method tests)"
|
76
|
+
end
|
77
|
+
|
78
|
+
flag :help do
|
79
|
+
short "-h"
|
80
|
+
long "--help"
|
81
|
+
desc "Show help message"
|
82
|
+
end
|
83
|
+
|
84
|
+
def initialize
|
85
|
+
@prompt = TTY::Prompt.new
|
86
|
+
@pastel = Pastel.new
|
87
|
+
@terminal_width = begin
|
88
|
+
TTY::Screen.width
|
89
|
+
rescue StandardError
|
90
|
+
80
|
91
|
+
end
|
92
|
+
@config_initialized = false
|
93
|
+
@http_client = nil
|
94
|
+
@testng = nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def start
|
98
|
+
normalized_argv = preprocess_arguments(ARGV.dup)
|
99
|
+
|
100
|
+
parse(normalized_argv)
|
101
|
+
|
102
|
+
if params[:help]
|
103
|
+
help_display = ShowHelp.new(@pastel, Tng::VERSION)
|
104
|
+
puts help_display.render
|
105
|
+
return
|
106
|
+
end
|
107
|
+
|
108
|
+
rails_loaded = load_rails_environment
|
109
|
+
|
110
|
+
initialize_config_and_clients if rails_loaded
|
111
|
+
|
112
|
+
if params[:type] && params[:file]
|
113
|
+
run_direct_generation
|
114
|
+
else
|
115
|
+
if find_rails_root && !rails_loaded && !defined?(Rails)
|
116
|
+
puts @pastel.decorate("#{Tng::UI::Theme.icon(:error)} Failed to load Rails environment.", Tng::UI::Theme.color(:error))
|
117
|
+
puts @pastel.decorate("Please ensure you're running this command from a Rails application directory.", Tng::UI::Theme.color(:warning))
|
118
|
+
puts @pastel.decorate("Try running: bundle exec tng", Tng::UI::Theme.color(:muted))
|
119
|
+
return
|
120
|
+
end
|
121
|
+
|
122
|
+
return unless check_configuration
|
123
|
+
|
124
|
+
check_system_status
|
125
|
+
clear_screen
|
126
|
+
puts DisplayBanner.new(@pastel, Tng::VERSION).render
|
127
|
+
main_menu
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def preprocess_arguments(argv)
|
132
|
+
normalized = []
|
133
|
+
|
134
|
+
argv.each do |arg|
|
135
|
+
case arg
|
136
|
+
when /^(type|t)=(.+)$/ # Handle type=value or t=value (without dash)
|
137
|
+
normalized << "--type=#{::Regexp.last_match(2)}"
|
138
|
+
when /^(file|f)=(.+)$/ # Handle file=value or f=value (without dash)
|
139
|
+
normalized << "--file=#{::Regexp.last_match(2)}"
|
140
|
+
when /^(method|m)=(.+)$/ # Handle method=value or m=value (without dash)
|
141
|
+
normalized << "--method=#{::Regexp.last_match(2)}"
|
142
|
+
when /^(help|h)=(.+)$/ # Handle help=value or h=value (without dash)
|
143
|
+
normalized << "--help=#{::Regexp.last_match(2)}"
|
144
|
+
when /^-([tf])=(.+)$/ # Handle -t=value or -f=value (convert to long form)
|
145
|
+
key = ::Regexp.last_match(1) == "t" ? "type" : "file"
|
146
|
+
normalized << "--#{key}=#{::Regexp.last_match(2)}"
|
147
|
+
when /^-([mh])=(.+)$/ # Handle -m=value or -h=value
|
148
|
+
key = ::Regexp.last_match(1) == "m" ? "method" : "help"
|
149
|
+
normalized << "--#{key}=#{::Regexp.last_match(2)}"
|
150
|
+
when /^--\w+=.+$/ # Handle --key=value (already correct)
|
151
|
+
normalized << arg
|
152
|
+
when /^-[tfmh]$/ # Handle single flags like -t, -f, -m, -h (keep as is)
|
153
|
+
normalized << arg
|
154
|
+
when /^--\w+$/ # Handle long flags like --help (keep as is)
|
155
|
+
normalized << arg
|
156
|
+
else
|
157
|
+
# Regular arguments or values
|
158
|
+
normalized << arg
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
normalized
|
163
|
+
end
|
164
|
+
|
165
|
+
def main_menu
|
166
|
+
loop do
|
167
|
+
clear_screen
|
168
|
+
|
169
|
+
puts DisplayBanner.new(@pastel, Tng::VERSION).render
|
170
|
+
|
171
|
+
begin
|
172
|
+
choice = @prompt.select(
|
173
|
+
center_text("Select an option:"),
|
174
|
+
cycle: true,
|
175
|
+
per_page: 5,
|
176
|
+
symbols: { marker: "→" }
|
177
|
+
) do |menu|
|
178
|
+
menu.choice "Generate tests", :tests
|
179
|
+
menu.choice "User stats", :stats
|
180
|
+
menu.choice "About", :about
|
181
|
+
menu.choice "Exit", :exit, key: "q"
|
182
|
+
end
|
183
|
+
|
184
|
+
handle_choice(choice)
|
185
|
+
rescue TTY::Reader::InputInterrupt, Interrupt
|
186
|
+
choice = :exit
|
187
|
+
end
|
188
|
+
|
189
|
+
break if choice == :exit
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def handle_choice(choice)
|
194
|
+
case choice
|
195
|
+
when :tests
|
196
|
+
handle_test_generation
|
197
|
+
when :stats
|
198
|
+
user_stats
|
199
|
+
when :about
|
200
|
+
show_about
|
201
|
+
when :exit
|
202
|
+
show_goodbye
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def handle_test_generation
|
207
|
+
choice = @prompt.select(
|
208
|
+
center_text("What would you like to test?"),
|
209
|
+
cycle: true,
|
210
|
+
per_page: 5,
|
211
|
+
symbols: { marker: "→" }
|
212
|
+
) do |menu|
|
213
|
+
menu.choice "Controller", :controller
|
214
|
+
menu.choice "Model", :model
|
215
|
+
menu.choice "Service", :service
|
216
|
+
menu.choice "Other", :other
|
217
|
+
menu.choice @pastel.decorate("#{Tng::UI::Theme.icon(:back)} Back", Tng::UI::Theme.color(:info)), :back
|
218
|
+
end
|
219
|
+
|
220
|
+
case choice
|
221
|
+
when :controller
|
222
|
+
generate_controller_tests
|
223
|
+
when :model
|
224
|
+
generate_model_tests
|
225
|
+
when :service
|
226
|
+
generate_service_tests
|
227
|
+
when :other
|
228
|
+
generate_other_tests
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def generate_controller_tests
|
233
|
+
header = @pastel.decorate("Scanning for controllers...", Tng::UI::Theme.color(:primary))
|
234
|
+
puts center_text(header)
|
235
|
+
|
236
|
+
spinner = TTY::Spinner.new(
|
237
|
+
center_text("[:spinner] Analyzing controllers..."),
|
238
|
+
format: :dots,
|
239
|
+
success_mark: @pastel.decorate(Tng::UI::Theme.icon(:success), Tng::UI::Theme.color(:success)),
|
240
|
+
error_mark: @pastel.decorate(Tng::UI::Theme.icon(:error), Tng::UI::Theme.color(:error))
|
241
|
+
)
|
242
|
+
spinner.auto_spin
|
243
|
+
controllers = Tng::Analyzers::Controller.files_in_dir("app/controllers").map do |file|
|
244
|
+
relative_path = file[:path].gsub(%r{^.*app/controllers/}, "").gsub(".rb", "")
|
245
|
+
namespaced_name = relative_path.split("/").map(&:camelize).join("::")
|
246
|
+
|
247
|
+
{
|
248
|
+
name: namespaced_name,
|
249
|
+
path: file[:path]
|
250
|
+
}
|
251
|
+
end
|
252
|
+
|
253
|
+
flow_display = ControllerTestFlowDisplay.new(@prompt, @pastel)
|
254
|
+
|
255
|
+
if controllers.empty?
|
256
|
+
spinner.error
|
257
|
+
flow_display.show_no_controllers_message
|
258
|
+
return
|
259
|
+
end
|
260
|
+
|
261
|
+
success_msg = @pastel.decorate("Found #{controllers.length} controllers", Tng::UI::Theme.color(:success))
|
262
|
+
spinner.success(center_text(success_msg))
|
263
|
+
|
264
|
+
controller_choice = flow_display.select_controller(controllers)
|
265
|
+
return if controller_choice == :back
|
266
|
+
|
267
|
+
show_controller_test_options(controller_choice)
|
268
|
+
end
|
269
|
+
|
270
|
+
def generate_model_tests
|
271
|
+
header = @pastel.decorate("Scanning for models...", Tng::UI::Theme.color(:primary))
|
272
|
+
puts center_text(header)
|
273
|
+
|
274
|
+
spinner = TTY::Spinner.new(
|
275
|
+
center_text("[:spinner] Analyzing models..."),
|
276
|
+
format: :dots,
|
277
|
+
success_mark: @pastel.decorate(Tng::UI::Theme.icon(:success), Tng::UI::Theme.color(:success)),
|
278
|
+
error_mark: @pastel.decorate(Tng::UI::Theme.icon(:error), Tng::UI::Theme.color(:error))
|
279
|
+
)
|
280
|
+
spinner.auto_spin
|
281
|
+
|
282
|
+
models = Tng::Analyzers::Model.files_in_dir("app/models").map do |file|
|
283
|
+
relative_path = file[:path].gsub(%r{^.*app/models/}, "").gsub(".rb", "")
|
284
|
+
namespaced_name = relative_path.split("/").map(&:camelize).join("::")
|
285
|
+
|
286
|
+
{
|
287
|
+
name: namespaced_name,
|
288
|
+
path: file[:path]
|
289
|
+
}
|
290
|
+
end
|
291
|
+
|
292
|
+
flow_display = ModelTestFlowDisplay.new(@prompt, @pastel)
|
293
|
+
|
294
|
+
if models.empty?
|
295
|
+
spinner.error
|
296
|
+
flow_display.show_no_models_message
|
297
|
+
return
|
298
|
+
end
|
299
|
+
|
300
|
+
success_msg = @pastel.decorate("Found #{models.length} models", Tng::UI::Theme.color(:success))
|
301
|
+
spinner.success(center_text(success_msg))
|
302
|
+
|
303
|
+
model_choice = flow_display.select_model(models)
|
304
|
+
return if model_choice == :back
|
305
|
+
|
306
|
+
show_model_test_options(model_choice)
|
307
|
+
end
|
308
|
+
|
309
|
+
def generate_service_tests
|
310
|
+
header = @pastel.decorate("Scanning for services...", Tng::UI::Theme.color(:primary))
|
311
|
+
puts center_text(header)
|
312
|
+
|
313
|
+
spinner = TTY::Spinner.new(
|
314
|
+
center_text("[:spinner] Analyzing services..."),
|
315
|
+
format: :dots,
|
316
|
+
success_mark: @pastel.decorate(Tng::UI::Theme.icon(:success), Tng::UI::Theme.color(:success)),
|
317
|
+
error_mark: @pastel.decorate(Tng::UI::Theme.icon(:error), Tng::UI::Theme.color(:error))
|
318
|
+
)
|
319
|
+
spinner.auto_spin
|
320
|
+
|
321
|
+
services = Tng::Analyzers::Service.files_in_dir.map do |file|
|
322
|
+
relative_path = file[:path].gsub(%r{^.*app/services?/}, "").gsub(".rb", "")
|
323
|
+
namespaced_name = relative_path.split("/").map(&:camelize).join("::")
|
324
|
+
|
325
|
+
{
|
326
|
+
name: namespaced_name,
|
327
|
+
path: file[:path]
|
328
|
+
}
|
329
|
+
end
|
330
|
+
|
331
|
+
flow_display = ServiceTestFlowDisplay.new(@prompt, @pastel)
|
332
|
+
|
333
|
+
if services.empty?
|
334
|
+
spinner.error
|
335
|
+
flow_display.show_no_services_message
|
336
|
+
return
|
337
|
+
end
|
338
|
+
|
339
|
+
success_msg = @pastel.decorate("Found #{services.length} services", Tng::UI::Theme.color(:success))
|
340
|
+
spinner.success(center_text(success_msg))
|
341
|
+
|
342
|
+
service_choice = flow_display.select_service(services)
|
343
|
+
return if service_choice == :back
|
344
|
+
|
345
|
+
show_service_test_options(service_choice)
|
346
|
+
end
|
347
|
+
|
348
|
+
def generate_other_tests
|
349
|
+
header = @pastel.decorate("Scanning for other files...", Tng::UI::Theme.color(:primary))
|
350
|
+
puts center_text(header)
|
351
|
+
|
352
|
+
spinner = TTY::Spinner.new(
|
353
|
+
center_text("[:spinner] Analyzing other files..."),
|
354
|
+
format: :dots,
|
355
|
+
success_mark: @pastel.decorate(Tng::UI::Theme.icon(:success), Tng::UI::Theme.color(:success)),
|
356
|
+
error_mark: @pastel.decorate(Tng::UI::Theme.icon(:error), Tng::UI::Theme.color(:error))
|
357
|
+
)
|
358
|
+
spinner.auto_spin
|
359
|
+
|
360
|
+
other_files = Tng::Analyzers::Other.files_in_dir.map do |file|
|
361
|
+
relative_path = file[:relative_path].gsub(".rb", "")
|
362
|
+
namespaced_name = relative_path.split("/").map(&:camelize).join("::")
|
363
|
+
|
364
|
+
{
|
365
|
+
name: namespaced_name,
|
366
|
+
path: file[:path],
|
367
|
+
type: file[:type]
|
368
|
+
}
|
369
|
+
end
|
370
|
+
|
371
|
+
flow_display = OtherTestFlowDisplay.new(@prompt, @pastel)
|
372
|
+
|
373
|
+
if other_files.empty?
|
374
|
+
spinner.error
|
375
|
+
flow_display.show_no_other_files_message
|
376
|
+
return
|
377
|
+
end
|
378
|
+
|
379
|
+
success_msg = @pastel.decorate("Found #{other_files.length} other files", Tng::UI::Theme.color(:success))
|
380
|
+
spinner.success(center_text(success_msg))
|
381
|
+
|
382
|
+
other_choice = flow_display.select_other_file(other_files)
|
383
|
+
return if other_choice == :back
|
384
|
+
|
385
|
+
show_other_test_options(other_choice)
|
386
|
+
end
|
387
|
+
|
388
|
+
def show_controller_method_selection(controller)
|
389
|
+
header = @pastel.decorate("#{Tng::UI::Theme.icon(:info)} Analyzing methods for #{controller[:name]}", Tng::UI::Theme.color(:primary))
|
390
|
+
puts center_text(header)
|
391
|
+
|
392
|
+
methods = extract_controller_methods(controller)
|
393
|
+
flow_display = ControllerTestFlowDisplay.new(@prompt, @pastel)
|
394
|
+
|
395
|
+
if methods.empty?
|
396
|
+
flow_display.show_no_methods_message(controller)
|
397
|
+
return
|
398
|
+
end
|
399
|
+
|
400
|
+
method_choice = flow_display.select_controller_method(controller, methods)
|
401
|
+
|
402
|
+
return if method_choice == :back
|
403
|
+
|
404
|
+
generate_test_for_controller_method(controller, method_choice)
|
405
|
+
end
|
406
|
+
|
407
|
+
def generate_test_for_controller_method(controller, method_info)
|
408
|
+
header = @pastel.decorate(
|
409
|
+
"#{Tng::UI::Theme.icon(:marker)} Generating test for #{controller[:name]}##{method_info[:name]}", Tng::UI::Theme.color(:primary)
|
410
|
+
)
|
411
|
+
puts center_text(header)
|
412
|
+
|
413
|
+
analysis_msg = @pastel.decorate("Analyzing method structure...", Tng::UI::Theme.color(:muted))
|
414
|
+
puts center_text(analysis_msg)
|
415
|
+
|
416
|
+
progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
|
417
|
+
progress_bar = TTY::ProgressBar.new(
|
418
|
+
"#{" " * progress_padding}[:bar] :percent :current/:total",
|
419
|
+
total: 5,
|
420
|
+
bar_format: :box,
|
421
|
+
complete: @pastel.decorate("█", Tng::UI::Theme.color(:success)),
|
422
|
+
incomplete: @pastel.decorate("░", Tng::UI::Theme.color(:muted)),
|
423
|
+
width: 40
|
424
|
+
)
|
425
|
+
|
426
|
+
step_msg = @pastel.decorate("Parsing method details...", Tng::UI::Theme.color(:muted))
|
427
|
+
puts center_text(step_msg)
|
428
|
+
progress_bar.advance(1)
|
429
|
+
sleep(0.3)
|
430
|
+
|
431
|
+
step_msg = @pastel.decorate("Analyzing method dependencies...", Tng::UI::Theme.color(:muted))
|
432
|
+
puts center_text(step_msg)
|
433
|
+
progress_bar.advance(1)
|
434
|
+
sleep(0.3)
|
435
|
+
|
436
|
+
step_msg = @pastel.decorate("Extracting method context...", Tng::UI::Theme.color(:muted))
|
437
|
+
puts center_text(step_msg)
|
438
|
+
progress_bar.advance(1)
|
439
|
+
sleep(0.3)
|
440
|
+
|
441
|
+
step_msg = @pastel.decorate("Generating method test code...", Tng::UI::Theme.color(:muted))
|
442
|
+
puts center_text(step_msg)
|
443
|
+
progress_bar.advance(1)
|
444
|
+
result = Tng::Services::TestGenerator.new(@http_client).run_for_controller_method(controller, method_info)
|
445
|
+
sleep(0.5)
|
446
|
+
|
447
|
+
step_msg = @pastel.decorate("Writing test file...", Tng::UI::Theme.color(:muted))
|
448
|
+
puts center_text(step_msg)
|
449
|
+
progress_bar.advance(1)
|
450
|
+
sleep(0.2)
|
451
|
+
|
452
|
+
# Show completion message with test count and timing
|
453
|
+
if result && result[:test_count]
|
454
|
+
count_msg = result[:test_count] == 1 ? "1 test" : "#{result[:test_count]} tests"
|
455
|
+
time_msg = result[:generation_time] ? " in #{format_generation_time(result[:generation_time])}" : ""
|
456
|
+
completion_msg = @pastel.decorate(
|
457
|
+
"#{Tng::UI::Theme.icon(:success)} Generated #{count_msg} for controller method#{time_msg}!", Tng::UI::Theme.color(:success)
|
458
|
+
)
|
459
|
+
else
|
460
|
+
completion_msg = @pastel.decorate("#{Tng::UI::Theme.icon(:success)} Method test generation completed!", Tng::UI::Theme.color(:success))
|
461
|
+
end
|
462
|
+
puts center_text(completion_msg)
|
463
|
+
|
464
|
+
if result && result[:file_path]
|
465
|
+
show_post_generation_menu(result)
|
466
|
+
else
|
467
|
+
@prompt.keypress(center_text(@pastel.decorate("Press any key to continue...", Tng::UI::Theme.color(:muted))))
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
def show_controller_test_options(controller_choice)
|
472
|
+
return unless check_authentication_configuration
|
473
|
+
|
474
|
+
show_controller_method_selection(controller_choice)
|
475
|
+
end
|
476
|
+
|
477
|
+
def show_model_test_options(model_choice)
|
478
|
+
return unless check_authentication_configuration
|
479
|
+
|
480
|
+
show_model_method_selection(model_choice)
|
481
|
+
end
|
482
|
+
|
483
|
+
def show_service_test_options(service_choice)
|
484
|
+
return unless check_authentication_configuration
|
485
|
+
|
486
|
+
show_service_method_selection(service_choice)
|
487
|
+
end
|
488
|
+
|
489
|
+
def show_model_method_selection(model)
|
490
|
+
header = @pastel.decorate("#{Tng::UI::Theme.icon(:info)} Analyzing methods for #{model[:name]}", Tng::UI::Theme.color(:primary))
|
491
|
+
puts center_text(header)
|
492
|
+
|
493
|
+
methods = extract_model_methods(model)
|
494
|
+
flow_display = ModelTestFlowDisplay.new(@prompt, @pastel)
|
495
|
+
|
496
|
+
if methods.empty?
|
497
|
+
flow_display.show_no_methods_message(model)
|
498
|
+
return
|
499
|
+
end
|
500
|
+
|
501
|
+
method_choice = flow_display.select_model_method(model, methods)
|
502
|
+
|
503
|
+
return if method_choice == :back
|
504
|
+
|
505
|
+
generate_test_for_model_method(model, method_choice)
|
506
|
+
end
|
507
|
+
|
508
|
+
def generate_test_for_model_method(model, method_info)
|
509
|
+
header = @pastel.decorate(
|
510
|
+
"#{Tng::UI::Theme.icon(:marker)} Generating test for #{model[:name]}##{method_info[:name]}", Tng::UI::Theme.color(:primary)
|
511
|
+
)
|
512
|
+
puts center_text(header)
|
513
|
+
|
514
|
+
analysis_msg = @pastel.decorate("Analyzing method structure...", Tng::UI::Theme.color(:muted))
|
515
|
+
puts center_text(analysis_msg)
|
516
|
+
|
517
|
+
progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
|
518
|
+
progress_bar = TTY::ProgressBar.new(
|
519
|
+
"#{" " * progress_padding}[:bar] :percent :current/:total",
|
520
|
+
total: 4,
|
521
|
+
bar_format: :box,
|
522
|
+
complete: @pastel.decorate("█", Tng::UI::Theme.color(:success)),
|
523
|
+
incomplete: @pastel.decorate("░", Tng::UI::Theme.color(:muted)),
|
524
|
+
width: 40
|
525
|
+
)
|
526
|
+
|
527
|
+
step_msg = @pastel.decorate("Parsing method details...", Tng::UI::Theme.color(:muted))
|
528
|
+
puts center_text(step_msg)
|
529
|
+
progress_bar.advance(1)
|
530
|
+
sleep(0.3)
|
531
|
+
|
532
|
+
step_msg = @pastel.decorate("Analyzing method dependencies...", Tng::UI::Theme.color(:muted))
|
533
|
+
puts center_text(step_msg)
|
534
|
+
progress_bar.advance(1)
|
535
|
+
sleep(0.3)
|
536
|
+
|
537
|
+
step_msg = @pastel.decorate("Generating method test code...", Tng::UI::Theme.color(:muted))
|
538
|
+
puts center_text(step_msg)
|
539
|
+
progress_bar.advance(1)
|
540
|
+
result = Tng::Services::TestGenerator.new(@http_client).run_for_model_method(model, method_info)
|
541
|
+
sleep(0.5)
|
542
|
+
|
543
|
+
step_msg = @pastel.decorate("Writing test file...", Tng::UI::Theme.color(:muted))
|
544
|
+
puts center_text(step_msg)
|
545
|
+
progress_bar.advance(1)
|
546
|
+
sleep(0.2)
|
547
|
+
|
548
|
+
# Show completion message with test count and timing
|
549
|
+
if result && result[:test_count]
|
550
|
+
count_msg = result[:test_count] == 1 ? "1 test" : "#{result[:test_count]} tests"
|
551
|
+
time_msg = result[:generation_time] ? " in #{format_generation_time(result[:generation_time])}" : ""
|
552
|
+
completion_msg = @pastel.decorate(
|
553
|
+
"#{Tng::UI::Theme.icon(:success)} Generated #{count_msg} for model method#{time_msg}!", Tng::UI::Theme.color(:success)
|
554
|
+
)
|
555
|
+
else
|
556
|
+
completion_msg = @pastel.decorate("#{Tng::UI::Theme.icon(:success)} Method test generation completed!", Tng::UI::Theme.color(:success))
|
557
|
+
end
|
558
|
+
puts center_text(completion_msg)
|
559
|
+
|
560
|
+
if result && result[:file_path]
|
561
|
+
show_post_generation_menu(result)
|
562
|
+
else
|
563
|
+
@prompt.keypress(center_text(@pastel.decorate("Press any key to continue...", Tng::UI::Theme.color(:muted))))
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
def generate_test_for_service_method(service, method_info)
|
568
|
+
header = @pastel.decorate(
|
569
|
+
"#{Tng::UI::Theme.icon(:marker)} Generating test for #{service[:name]}##{method_info[:name]}", Tng::UI::Theme.color(:primary)
|
570
|
+
)
|
571
|
+
puts center_text(header)
|
572
|
+
|
573
|
+
analysis_msg = @pastel.decorate("Analyzing method structure...", Tng::UI::Theme.color(:muted))
|
574
|
+
puts center_text(analysis_msg)
|
575
|
+
|
576
|
+
progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
|
577
|
+
progress_bar = TTY::ProgressBar.new(
|
578
|
+
"#{" " * progress_padding}[:bar] :percent :current/:total",
|
579
|
+
total: 4,
|
580
|
+
bar_format: :box,
|
581
|
+
complete: @pastel.decorate("█", Tng::UI::Theme.color(:success)),
|
582
|
+
incomplete: @pastel.decorate("░", Tng::UI::Theme.color(:muted)),
|
583
|
+
width: 40
|
584
|
+
)
|
585
|
+
|
586
|
+
step_msg = @pastel.decorate("Parsing method details...", Tng::UI::Theme.color(:muted))
|
587
|
+
puts center_text(step_msg)
|
588
|
+
progress_bar.advance(1)
|
589
|
+
sleep(0.3)
|
590
|
+
|
591
|
+
step_msg = @pastel.decorate("Analyzing method dependencies...", Tng::UI::Theme.color(:muted))
|
592
|
+
puts center_text(step_msg)
|
593
|
+
progress_bar.advance(1)
|
594
|
+
sleep(0.3)
|
595
|
+
|
596
|
+
step_msg = @pastel.decorate("Generating method test code...", Tng::UI::Theme.color(:muted))
|
597
|
+
puts center_text(step_msg)
|
598
|
+
progress_bar.advance(1)
|
599
|
+
result = Tng::Services::TestGenerator.new(@http_client).run_for_service_method(service, method_info)
|
600
|
+
sleep(0.5)
|
601
|
+
|
602
|
+
step_msg = @pastel.decorate("Writing test file...", Tng::UI::Theme.color(:muted))
|
603
|
+
puts center_text(step_msg)
|
604
|
+
progress_bar.advance(1)
|
605
|
+
sleep(0.2)
|
606
|
+
|
607
|
+
# Show completion message with test count and timing
|
608
|
+
if result && result[:test_count]
|
609
|
+
count_msg = result[:test_count] == 1 ? "1 test" : "#{result[:test_count]} tests"
|
610
|
+
time_msg = result[:generation_time] ? " in #{format_generation_time(result[:generation_time])}" : ""
|
611
|
+
completion_msg = @pastel.decorate(
|
612
|
+
"#{Tng::UI::Theme.icon(:success)} Generated #{count_msg} for service method#{time_msg}!", Tng::UI::Theme.color(:success)
|
613
|
+
)
|
614
|
+
else
|
615
|
+
completion_msg = @pastel.decorate("#{Tng::UI::Theme.icon(:success)} Method test generation completed!", Tng::UI::Theme.color(:success))
|
616
|
+
end
|
617
|
+
puts center_text(completion_msg)
|
618
|
+
|
619
|
+
if result && result[:file_path]
|
620
|
+
show_post_generation_menu(result)
|
621
|
+
else
|
622
|
+
@prompt.keypress(center_text(@pastel.decorate("Press any key to continue...", Tng::UI::Theme.color(:muted))))
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
def show_service_method_selection(service)
|
627
|
+
header = @pastel.decorate("#{Tng::UI::Theme.icon(:info)} Analyzing methods for #{service[:name]}", Tng::UI::Theme.color(:primary))
|
628
|
+
puts center_text(header)
|
629
|
+
|
630
|
+
methods = extract_service_methods(service)
|
631
|
+
flow_display = ServiceTestFlowDisplay.new(@prompt, @pastel)
|
632
|
+
|
633
|
+
if methods.empty?
|
634
|
+
flow_display.show_no_methods_message(service)
|
635
|
+
return
|
636
|
+
end
|
637
|
+
|
638
|
+
method_choice = flow_display.select_service_method(service, methods)
|
639
|
+
|
640
|
+
return if method_choice == :back
|
641
|
+
|
642
|
+
generate_test_for_service_method(service, method_choice)
|
643
|
+
end
|
644
|
+
|
645
|
+
def show_other_test_options(other_choice)
|
646
|
+
return unless check_authentication_configuration
|
647
|
+
|
648
|
+
show_other_method_selection(other_choice)
|
649
|
+
end
|
650
|
+
|
651
|
+
def show_other_method_selection(other_file)
|
652
|
+
header = @pastel.decorate("#{Tng::UI::Theme.icon(:info)} Analyzing methods for #{other_file[:name]}", Tng::UI::Theme.color(:primary))
|
653
|
+
puts center_text(header)
|
654
|
+
|
655
|
+
methods = extract_other_methods(other_file)
|
656
|
+
flow_display = OtherTestFlowDisplay.new(@prompt, @pastel)
|
657
|
+
|
658
|
+
if methods.empty?
|
659
|
+
flow_display.show_no_methods_message(other_file)
|
660
|
+
return
|
661
|
+
end
|
662
|
+
|
663
|
+
method_choice = flow_display.select_other_method(other_file, methods)
|
664
|
+
|
665
|
+
return if method_choice == :back
|
666
|
+
|
667
|
+
generate_test_for_other_method(other_file, method_choice)
|
668
|
+
end
|
669
|
+
|
670
|
+
def generate_test_for_other_method(other_file, method_info)
|
671
|
+
header = @pastel.decorate(
|
672
|
+
"#{Tng::UI::Theme.icon(:marker)} Generating test for #{other_file[:name]}##{method_info[:name]}", Tng::UI::Theme.color(:primary)
|
673
|
+
)
|
674
|
+
puts center_text(header)
|
675
|
+
|
676
|
+
analysis_msg = @pastel.decorate("Analyzing method structure...", Tng::UI::Theme.color(:muted))
|
677
|
+
puts center_text(analysis_msg)
|
678
|
+
|
679
|
+
progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
|
680
|
+
progress_bar = TTY::ProgressBar.new(
|
681
|
+
"#{" " * progress_padding}[:bar] :percent :current/:total",
|
682
|
+
total: 4,
|
683
|
+
bar_format: :box,
|
684
|
+
complete: @pastel.decorate("█", Tng::UI::Theme.color(:success)),
|
685
|
+
incomplete: @pastel.decorate("░", Tng::UI::Theme.color(:muted)),
|
686
|
+
width: 40
|
687
|
+
)
|
688
|
+
|
689
|
+
step_msg = @pastel.decorate("Parsing method details...", Tng::UI::Theme.color(:muted))
|
690
|
+
puts center_text(step_msg)
|
691
|
+
progress_bar.advance(1)
|
692
|
+
sleep(0.3)
|
693
|
+
|
694
|
+
step_msg = @pastel.decorate("Analyzing method dependencies...", Tng::UI::Theme.color(:muted))
|
695
|
+
puts center_text(step_msg)
|
696
|
+
progress_bar.advance(1)
|
697
|
+
sleep(0.3)
|
698
|
+
|
699
|
+
step_msg = @pastel.decorate("Generating method test code...", Tng::UI::Theme.color(:muted))
|
700
|
+
puts center_text(step_msg)
|
701
|
+
progress_bar.advance(1)
|
702
|
+
result = Tng::Services::TestGenerator.new(@http_client).run_for_other_method(other_file, method_info)
|
703
|
+
sleep(0.5)
|
704
|
+
|
705
|
+
step_msg = @pastel.decorate("Writing test file...", Tng::UI::Theme.color(:muted))
|
706
|
+
puts center_text(step_msg)
|
707
|
+
progress_bar.advance(1)
|
708
|
+
sleep(0.2)
|
709
|
+
|
710
|
+
# Show completion message with test count and timing
|
711
|
+
if result && result[:test_count]
|
712
|
+
count_msg = result[:test_count] == 1 ? "1 test" : "#{result[:test_count]} tests"
|
713
|
+
time_msg = result[:generation_time] ? " in #{format_generation_time(result[:generation_time])}" : ""
|
714
|
+
completion_msg = @pastel.decorate(
|
715
|
+
"#{Tng::UI::Theme.icon(:success)} Generated #{count_msg} for other method#{time_msg}!", Tng::UI::Theme.color(:success)
|
716
|
+
)
|
717
|
+
else
|
718
|
+
completion_msg = @pastel.decorate("#{Tng::UI::Theme.icon(:success)} Method test generation completed!", Tng::UI::Theme.color(:success))
|
719
|
+
end
|
720
|
+
puts center_text(completion_msg)
|
721
|
+
|
722
|
+
if result && result[:file_path]
|
723
|
+
show_post_generation_menu(result)
|
724
|
+
else
|
725
|
+
@prompt.keypress(center_text(@pastel.decorate("Press any key to continue...", Tng::UI::Theme.color(:muted))))
|
726
|
+
end
|
727
|
+
end
|
728
|
+
|
729
|
+
# STATS
|
730
|
+
def user_stats
|
731
|
+
stats_data = @testng.get_user_stats
|
732
|
+
UserStatsDisplay.new(@pastel, @prompt).display(stats_data)
|
733
|
+
end
|
734
|
+
|
735
|
+
def check_system_status
|
736
|
+
unless params[:type] && params[:file]
|
737
|
+
puts @pastel.decorate("#{Tng::UI::Theme.icon(:info)} Checking system status...",
|
738
|
+
Tng::UI::Theme.color(:muted))
|
739
|
+
end
|
740
|
+
|
741
|
+
status = @testng.check_system_status
|
742
|
+
status_ok = SystemStatusDisplay.new(@pastel, params).display(status)
|
743
|
+
|
744
|
+
return if status_ok
|
745
|
+
|
746
|
+
puts
|
747
|
+
puts @pastel.decorate("Press any key to exit...", Tng::UI::Theme.color(:muted))
|
748
|
+
$stdin.getch
|
749
|
+
exit(1)
|
750
|
+
end
|
751
|
+
|
752
|
+
def check_configuration
|
753
|
+
return true if Tng::Services::UserAppConfig.configured?
|
754
|
+
|
755
|
+
missing = Tng::Services::UserAppConfig.missing_config
|
756
|
+
|
757
|
+
ConfigurationDisplay.new(@pastel).display_missing_config(missing)
|
758
|
+
false
|
759
|
+
end
|
760
|
+
|
761
|
+
def check_authentication_configuration
|
762
|
+
auth_warning = AuthenticationWarningDisplay.new(@pastel)
|
763
|
+
auth_missing = auth_warning.display_if_missing
|
764
|
+
|
765
|
+
return false if auth_missing
|
766
|
+
|
767
|
+
true
|
768
|
+
end
|
769
|
+
|
770
|
+
# ABOUT
|
771
|
+
def show_about
|
772
|
+
AboutDisplay.new(@pastel, @prompt, Tng::VERSION).display
|
773
|
+
end
|
774
|
+
|
775
|
+
# GOODBYE
|
776
|
+
def show_goodbye
|
777
|
+
clear_screen
|
778
|
+
GoodbyeDisplay.new(@pastel).display
|
779
|
+
end
|
780
|
+
|
781
|
+
def run_direct_generation
|
782
|
+
return unless check_configuration
|
783
|
+
|
784
|
+
return unless check_authentication_configuration
|
785
|
+
|
786
|
+
status = @testng.check_system_status
|
787
|
+
unless SystemStatusDisplay.new(@pastel, params).display(status)
|
788
|
+
puts @pastel.red("❌ System status check failed")
|
789
|
+
return
|
790
|
+
end
|
791
|
+
|
792
|
+
direct_generator = Tng::Services::DirectGeneration.new(
|
793
|
+
@pastel,
|
794
|
+
@testng,
|
795
|
+
@http_client,
|
796
|
+
params,
|
797
|
+
method(:show_post_generation_menu)
|
798
|
+
)
|
799
|
+
|
800
|
+
direct_generator.run
|
801
|
+
end
|
802
|
+
|
803
|
+
def show_post_generation_menu(result)
|
804
|
+
is_direct_mode = params[:type] && params[:file]
|
805
|
+
|
806
|
+
if is_direct_mode
|
807
|
+
puts
|
808
|
+
header = @pastel.decorate("#{Tng::UI::Theme.icon(:rocket)} Test Generated Successfully!", Tng::UI::Theme.color(:primary))
|
809
|
+
puts center_text(header)
|
810
|
+
puts
|
811
|
+
|
812
|
+
info_msg = [
|
813
|
+
@pastel.decorate("#{Tng::UI::Theme.icon(:config)} File: #{result[:file_path]}", Tng::UI::Theme.color(:info)),
|
814
|
+
@pastel.decorate("#{Tng::UI::Theme.icon(:marker)} Run: #{result[:run_command]}", Tng::UI::Theme.color(:warning))
|
815
|
+
].join("\n")
|
816
|
+
puts center_text(info_msg)
|
817
|
+
puts
|
818
|
+
|
819
|
+
puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:success)} Test generation completed successfully!", Tng::UI::Theme.color(:success)))
|
820
|
+
return
|
821
|
+
end
|
822
|
+
|
823
|
+
loop do
|
824
|
+
puts
|
825
|
+
header = @pastel.decorate("#{Tng::UI::Theme.icon(:rocket)} Test Generated Successfully!", Tng::UI::Theme.color(:primary))
|
826
|
+
puts center_text(header)
|
827
|
+
puts
|
828
|
+
|
829
|
+
info_msg = [
|
830
|
+
@pastel.decorate("#{Tng::UI::Theme.icon(:config)} File: #{result[:file_path]}", Tng::UI::Theme.color(:info)),
|
831
|
+
@pastel.decorate("#{Tng::UI::Theme.icon(:marker)} Run: #{result[:run_command]}", Tng::UI::Theme.color(:warning))
|
832
|
+
].join("\n")
|
833
|
+
puts center_text(info_msg)
|
834
|
+
puts
|
835
|
+
|
836
|
+
choice = @prompt.select(
|
837
|
+
@pastel.decorate("What would you like to do?", Tng::UI::Theme.color(:primary)),
|
838
|
+
cycle: true,
|
839
|
+
symbols: { marker: Tng::UI::Theme.icon(:marker) }
|
840
|
+
) do |menu|
|
841
|
+
menu.choice @pastel.decorate("#{Tng::UI::Theme.icon(:config)} Copy run command", Tng::UI::Theme.color(:warning)),
|
842
|
+
:copy_command
|
843
|
+
menu.choice @pastel.decorate("#{Tng::UI::Theme.icon(:back)} Back to main menu", Tng::UI::Theme.color(:info)),
|
844
|
+
:back
|
845
|
+
end
|
846
|
+
|
847
|
+
case choice
|
848
|
+
when :copy_command
|
849
|
+
copy_to_clipboard(result[:run_command])
|
850
|
+
when :back
|
851
|
+
break
|
852
|
+
end
|
853
|
+
end
|
854
|
+
end
|
855
|
+
|
856
|
+
def initialize_config_and_clients
|
857
|
+
@config_initialized = true
|
858
|
+
|
859
|
+
unless Tng::Services::UserAppConfig.configured?
|
860
|
+
missing = Tng::Services::UserAppConfig.missing_config
|
861
|
+
ConfigurationDisplay.new(@pastel).display_missing_config(missing)
|
862
|
+
exit 1
|
863
|
+
end
|
864
|
+
|
865
|
+
@http_client = Tng::HttpClient.new(
|
866
|
+
Tng::Services::UserAppConfig.base_url,
|
867
|
+
Tng::Services::UserAppConfig.api_key
|
868
|
+
)
|
869
|
+
@testng = Services::Testng.new(@http_client)
|
870
|
+
end
|
871
|
+
|
872
|
+
private
|
873
|
+
|
874
|
+
def format_generation_time(seconds)
|
875
|
+
if seconds < 1
|
876
|
+
"#{(seconds * 1000).round}ms"
|
877
|
+
elsif seconds < 60
|
878
|
+
"#{seconds.round(1)}s"
|
879
|
+
else
|
880
|
+
minutes = (seconds / 60).floor
|
881
|
+
remaining_seconds = (seconds % 60).round
|
882
|
+
"#{minutes}m #{remaining_seconds}s"
|
883
|
+
end
|
884
|
+
end
|
885
|
+
end
|
886
|
+
|
887
|
+
cli = CLI.new
|
888
|
+
cli.start
|