tng 0.5.4 → 0.5.5
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 +4 -4
- data/README.md +19 -0
- data/bin/tng +446 -57
- data/binaries/go-ui-darwin-amd64 +0 -0
- data/binaries/go-ui-darwin-arm64 +0 -0
- data/binaries/go-ui-linux-amd64 +0 -0
- data/binaries/go-ui-linux-arm64 +0 -0
- data/binaries/tng-darwin-arm64.bundle +0 -0
- data/binaries/tng-darwin-x86_64.bundle +0 -0
- data/binaries/tng-linux-arm64.so +0 -0
- data/binaries/tng-linux-x86_64.so +0 -0
- data/binaries/tng.bundle +0 -0
- data/lib/generators/tng/install_generator.rb +11 -54
- data/lib/tng/api/http_client.rb +0 -2
- data/lib/tng/services/direct_generation.rb +33 -3
- data/lib/tng/services/file_discovery.rb +43 -0
- data/lib/tng/services/file_type_detector.rb +19 -2
- data/lib/tng/services/installer.rb +82 -0
- data/lib/tng/services/test_generator.rb +155 -22
- data/lib/tng/services/testng.rb +3 -1
- data/lib/tng/services/user_app_config.rb +3 -2
- data/lib/tng/ui/go_ui_session.rb +20 -1
- data/lib/tng/ui/post_install_box.rb +3 -3
- data/lib/tng/utils.rb +45 -6
- data/lib/tng/version.rb +1 -1
- data/lib/tng.rb +1 -1
- metadata +5 -3
data/bin/tng
CHANGED
|
@@ -7,10 +7,13 @@ lib_path = File.expand_path("../lib", __dir__)
|
|
|
7
7
|
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
|
8
8
|
|
|
9
9
|
require "tng"
|
|
10
|
+
require "json"
|
|
10
11
|
require "tng/services/direct_generation"
|
|
11
12
|
require "tng/services/extract_methods"
|
|
12
13
|
require "tng/services/file_type_detector"
|
|
14
|
+
require "tng/services/file_discovery"
|
|
13
15
|
require "tng/services/fix_orchestrator"
|
|
16
|
+
require "tng/services/installer"
|
|
14
17
|
|
|
15
18
|
require "tty-screen"
|
|
16
19
|
require "tty-option"
|
|
@@ -77,6 +80,9 @@ class CLI
|
|
|
77
80
|
example ""
|
|
78
81
|
example "Call Sites:", ""
|
|
79
82
|
example " bundle exec tng --file=app/services/payment_service.rb --method=process --callsites", ""
|
|
83
|
+
example ""
|
|
84
|
+
example "Initialize configuration:", ""
|
|
85
|
+
example " bundle exec tng init", ""
|
|
80
86
|
end
|
|
81
87
|
|
|
82
88
|
option :file do
|
|
@@ -183,6 +189,11 @@ class CLI
|
|
|
183
189
|
end
|
|
184
190
|
|
|
185
191
|
def start
|
|
192
|
+
if ARGV.first == "init" || ARGV.first == "install"
|
|
193
|
+
run_init_command
|
|
194
|
+
return
|
|
195
|
+
end
|
|
196
|
+
|
|
186
197
|
normalized_argv = preprocess_arguments(ARGV.dup)
|
|
187
198
|
|
|
188
199
|
parse(normalized_argv)
|
|
@@ -192,9 +203,13 @@ class CLI
|
|
|
192
203
|
return
|
|
193
204
|
end
|
|
194
205
|
|
|
195
|
-
|
|
206
|
+
rails_root = find_rails_root || Dir.pwd
|
|
207
|
+
@rails_project = Tng::Utils.rails_project?(rails_root)
|
|
208
|
+
@generic_mode = !@rails_project
|
|
196
209
|
|
|
197
|
-
|
|
210
|
+
rails_loaded = @rails_project ? load_rails_environment : false
|
|
211
|
+
|
|
212
|
+
initialize_config_and_clients if rails_loaded || @generic_mode
|
|
198
213
|
|
|
199
214
|
if params[:fix]
|
|
200
215
|
handle_fix_command
|
|
@@ -211,7 +226,7 @@ class CLI
|
|
|
211
226
|
elsif params[:file]
|
|
212
227
|
run_direct_generation
|
|
213
228
|
else
|
|
214
|
-
if
|
|
229
|
+
if @rails_project && !rails_loaded && !defined?(Rails)
|
|
215
230
|
puts @pastel.decorate("#{Tng::UI::Theme.icon(:error)} Failed to load Rails environment.", Tng::UI::Theme.color(:error))
|
|
216
231
|
puts @pastel.decorate("Please ensure you're running this command from a Rails application directory.", Tng::UI::Theme.color(:warning))
|
|
217
232
|
puts @pastel.decorate("Try running: bundle exec tng", Tng::UI::Theme.color(:muted))
|
|
@@ -344,7 +359,46 @@ class CLI
|
|
|
344
359
|
@go_ui.stop
|
|
345
360
|
end
|
|
346
361
|
|
|
362
|
+
def run_init_command
|
|
363
|
+
rails_root = find_rails_root
|
|
364
|
+
project_root = rails_root || Dir.pwd
|
|
365
|
+
rails_project = Tng::Utils.rails_project?(project_root)
|
|
366
|
+
target = if rails_project
|
|
367
|
+
File.join(project_root, "config", "initializers", "tng.rb")
|
|
368
|
+
else
|
|
369
|
+
File.join(project_root, "tng.rb")
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
if File.exist?(target)
|
|
373
|
+
puts "Configuration file already exists at #{target}"
|
|
374
|
+
print "Do you want to overwrite it? (y/n): "
|
|
375
|
+
answer = STDIN.gets&.strip&.downcase
|
|
376
|
+
if answer == "y" || answer == "yes"
|
|
377
|
+
_created, path = Tng::Services::Installer.install(
|
|
378
|
+
project_root: project_root,
|
|
379
|
+
rails_project: rails_project,
|
|
380
|
+
force: true
|
|
381
|
+
)
|
|
382
|
+
puts "TNG configuration file updated at #{path}"
|
|
383
|
+
else
|
|
384
|
+
puts "Skipping configuration file creation."
|
|
385
|
+
end
|
|
386
|
+
else
|
|
387
|
+
_created, path = Tng::Services::Installer.install(
|
|
388
|
+
project_root: project_root,
|
|
389
|
+
rails_project: rails_project,
|
|
390
|
+
force: false
|
|
391
|
+
)
|
|
392
|
+
puts "TNG configuration file created at #{path}"
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
347
396
|
def handle_test_generation
|
|
397
|
+
if @generic_mode
|
|
398
|
+
handle_generic_test_generation
|
|
399
|
+
return
|
|
400
|
+
end
|
|
401
|
+
|
|
348
402
|
choice = @go_ui.show_test_type_menu
|
|
349
403
|
|
|
350
404
|
case choice
|
|
@@ -360,6 +414,11 @@ class CLI
|
|
|
360
414
|
end
|
|
361
415
|
|
|
362
416
|
def handle_clone_detection
|
|
417
|
+
if @generic_mode
|
|
418
|
+
handle_generic_clone_detection
|
|
419
|
+
return
|
|
420
|
+
end
|
|
421
|
+
|
|
363
422
|
# Pick a file type first to narrow down
|
|
364
423
|
choice = @go_ui.show_test_type_menu("clones")
|
|
365
424
|
return if choice == "back"
|
|
@@ -496,6 +555,11 @@ class CLI
|
|
|
496
555
|
end
|
|
497
556
|
|
|
498
557
|
def handle_audit_method
|
|
558
|
+
if @generic_mode
|
|
559
|
+
handle_generic_audit
|
|
560
|
+
return
|
|
561
|
+
end
|
|
562
|
+
|
|
499
563
|
choice = @go_ui.show_test_type_menu("audit")
|
|
500
564
|
|
|
501
565
|
case choice
|
|
@@ -563,32 +627,40 @@ class CLI
|
|
|
563
627
|
end
|
|
564
628
|
|
|
565
629
|
def run_audit_for_controller_method(controller, method_info)
|
|
566
|
-
|
|
630
|
+
audit_result = nil
|
|
567
631
|
@go_ui.show_progress("Auditing #{controller[:name]}##{method_info[:name]}") do |progress|
|
|
568
632
|
progress.update("Analyzing method context...", 25)
|
|
569
633
|
progress.update("Running logical analysis...", 50)
|
|
570
634
|
progress.update("Detecting issues and behaviours...", 75)
|
|
571
635
|
|
|
572
|
-
|
|
636
|
+
audit_result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_controller_method(
|
|
573
637
|
controller, method_info, progress: progress
|
|
574
638
|
)
|
|
575
639
|
|
|
576
640
|
progress.update("Processing results...", 100)
|
|
577
641
|
|
|
578
|
-
if
|
|
579
|
-
{ message:
|
|
642
|
+
if audit_result&.dig(:error)
|
|
643
|
+
{ message: audit_result[:message] || "Audit failed", error: audit_result[:error] }
|
|
580
644
|
else
|
|
581
645
|
{ message: "Audit complete!" }
|
|
582
646
|
end
|
|
583
647
|
end
|
|
584
648
|
|
|
585
|
-
return @go_ui.show_auth_error(
|
|
649
|
+
return @go_ui.show_auth_error(audit_result[:message] || "Audit failed") if audit_result&.dig(:error)
|
|
586
650
|
|
|
587
|
-
display_audit_results(
|
|
651
|
+
display_audit_results(audit_result)
|
|
588
652
|
end
|
|
589
653
|
|
|
590
654
|
def display_audit_results(result)
|
|
591
|
-
audit_data = result[:audit_results]
|
|
655
|
+
audit_data = result.is_a?(Hash) ? (result[:audit_results] || result["audit_results"] || result[:result] || result["result"] || result) : result
|
|
656
|
+
if audit_data.is_a?(String)
|
|
657
|
+
begin
|
|
658
|
+
audit_data = JSON.parse(audit_data)
|
|
659
|
+
rescue JSON::ParserError
|
|
660
|
+
audit_data = nil
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
return if audit_data.nil?
|
|
592
664
|
|
|
593
665
|
# Send entire audit_data to go-ui (includes issues, behaviours, method_name, class_name, method_source_with_lines)
|
|
594
666
|
@go_ui.show_audit_results(audit_data, "issues")
|
|
@@ -784,29 +856,29 @@ class CLI
|
|
|
784
856
|
end
|
|
785
857
|
|
|
786
858
|
def run_audit_for_model_method(model, method_info)
|
|
787
|
-
|
|
859
|
+
audit_result = nil
|
|
788
860
|
@go_ui.show_progress("Auditing #{model[:name]}##{method_info[:name]}") do |progress|
|
|
789
861
|
progress.update("Parsing method details...", 25)
|
|
790
862
|
progress.update("Analyzing method context...", 25)
|
|
791
863
|
progress.update("Running logical analysis...", 50)
|
|
792
864
|
progress.update("Detecting issues and behaviours...", 75)
|
|
793
865
|
|
|
794
|
-
|
|
866
|
+
audit_result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_model_method(
|
|
795
867
|
model, method_info, progress: progress
|
|
796
868
|
)
|
|
797
869
|
|
|
798
870
|
progress.update("Processing results...", 100)
|
|
799
871
|
|
|
800
|
-
if
|
|
801
|
-
{ message:
|
|
872
|
+
if audit_result&.dig(:error)
|
|
873
|
+
{ message: audit_result[:message] || "Audit failed", error: audit_result[:error] }
|
|
802
874
|
else
|
|
803
875
|
{ message: "Audit complete!" }
|
|
804
876
|
end
|
|
805
877
|
end
|
|
806
878
|
|
|
807
|
-
return @go_ui.show_auth_error(
|
|
879
|
+
return @go_ui.show_auth_error(audit_result[:message] || "Audit failed") if audit_result&.dig(:error)
|
|
808
880
|
|
|
809
|
-
display_audit_results(
|
|
881
|
+
display_audit_results(audit_result)
|
|
810
882
|
end
|
|
811
883
|
|
|
812
884
|
def audit_service_method
|
|
@@ -860,29 +932,29 @@ class CLI
|
|
|
860
932
|
end
|
|
861
933
|
|
|
862
934
|
def run_audit_for_service_method(service, method_info)
|
|
863
|
-
|
|
935
|
+
audit_result = nil
|
|
864
936
|
@go_ui.show_progress("Auditing #{service[:name]}##{method_info[:name]}") do |progress|
|
|
865
937
|
progress.update("Parsing method details...", 25)
|
|
866
938
|
progress.update("Analyzing method context...", 25)
|
|
867
939
|
progress.update("Running logical analysis...", 50)
|
|
868
940
|
progress.update("Detecting issues and behaviours...", 75)
|
|
869
941
|
|
|
870
|
-
|
|
942
|
+
audit_result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_service_method(
|
|
871
943
|
service, method_info, progress: progress
|
|
872
944
|
)
|
|
873
945
|
|
|
874
946
|
progress.update("Processing results...", 100)
|
|
875
947
|
|
|
876
|
-
if
|
|
877
|
-
{ message:
|
|
948
|
+
if audit_result&.dig(:error)
|
|
949
|
+
{ message: audit_result[:message] || "Audit failed", error: audit_result[:error] }
|
|
878
950
|
else
|
|
879
951
|
{ message: "Audit complete!" }
|
|
880
952
|
end
|
|
881
953
|
end
|
|
882
954
|
|
|
883
|
-
return @go_ui.show_auth_error(
|
|
955
|
+
return @go_ui.show_auth_error(audit_result[:message] || "Audit failed") if audit_result&.dig(:error)
|
|
884
956
|
|
|
885
|
-
display_audit_results(
|
|
957
|
+
display_audit_results(audit_result)
|
|
886
958
|
end
|
|
887
959
|
|
|
888
960
|
def audit_other_method
|
|
@@ -934,29 +1006,29 @@ class CLI
|
|
|
934
1006
|
end
|
|
935
1007
|
|
|
936
1008
|
def run_audit_for_other_method(other_file, method_info)
|
|
937
|
-
|
|
1009
|
+
audit_result = nil
|
|
938
1010
|
@go_ui.show_progress("Auditing #{other_file[:name]}##{method_info[:name]}") do |progress|
|
|
939
1011
|
progress.update("Parsing method details...", 25)
|
|
940
1012
|
progress.update("Analyzing method context...", 25)
|
|
941
1013
|
progress.update("Running logical analysis...", 50)
|
|
942
1014
|
progress.update("Detecting issues and behaviours...", 75)
|
|
943
1015
|
|
|
944
|
-
|
|
1016
|
+
audit_result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_other_method(
|
|
945
1017
|
other_file, method_info, progress: progress
|
|
946
1018
|
)
|
|
947
1019
|
|
|
948
1020
|
progress.update("Processing results...", 100)
|
|
949
1021
|
|
|
950
|
-
if
|
|
951
|
-
{ message:
|
|
1022
|
+
if audit_result&.dig(:error)
|
|
1023
|
+
{ message: audit_result[:message] || "Audit failed", error: audit_result[:error] }
|
|
952
1024
|
else
|
|
953
1025
|
{ message: "Audit complete!" }
|
|
954
1026
|
end
|
|
955
1027
|
end
|
|
956
1028
|
|
|
957
|
-
return @go_ui.show_auth_error(
|
|
1029
|
+
return @go_ui.show_auth_error(audit_result[:message] || "Audit failed") if audit_result&.dig(:error)
|
|
958
1030
|
|
|
959
|
-
display_audit_results(
|
|
1031
|
+
display_audit_results(audit_result)
|
|
960
1032
|
end
|
|
961
1033
|
|
|
962
1034
|
def generate_controller_tests
|
|
@@ -1467,6 +1539,11 @@ class CLI
|
|
|
1467
1539
|
def initialize_config_and_clients
|
|
1468
1540
|
@config_initialized = true
|
|
1469
1541
|
|
|
1542
|
+
if @generic_mode
|
|
1543
|
+
config_path = File.join(Dir.pwd, "tng.rb")
|
|
1544
|
+
load config_path if File.exist?(config_path)
|
|
1545
|
+
end
|
|
1546
|
+
|
|
1470
1547
|
unless Tng::Services::UserAppConfig.configured?
|
|
1471
1548
|
missing = Tng::Services::UserAppConfig.missing_config
|
|
1472
1549
|
@go_ui.show_config_missing(missing)
|
|
@@ -1493,6 +1570,11 @@ class CLI
|
|
|
1493
1570
|
end
|
|
1494
1571
|
|
|
1495
1572
|
def handle_trace_method
|
|
1573
|
+
if @generic_mode
|
|
1574
|
+
handle_generic_trace
|
|
1575
|
+
return
|
|
1576
|
+
end
|
|
1577
|
+
|
|
1496
1578
|
choice = @go_ui.show_test_type_menu("trace")
|
|
1497
1579
|
|
|
1498
1580
|
case choice
|
|
@@ -1508,6 +1590,11 @@ class CLI
|
|
|
1508
1590
|
end
|
|
1509
1591
|
|
|
1510
1592
|
def handle_impact_method
|
|
1593
|
+
if @generic_mode
|
|
1594
|
+
handle_generic_impact
|
|
1595
|
+
return
|
|
1596
|
+
end
|
|
1597
|
+
|
|
1511
1598
|
choice = @go_ui.show_test_type_menu("impact")
|
|
1512
1599
|
|
|
1513
1600
|
case choice
|
|
@@ -1523,6 +1610,11 @@ class CLI
|
|
|
1523
1610
|
end
|
|
1524
1611
|
|
|
1525
1612
|
def handle_xray_method
|
|
1613
|
+
if @generic_mode
|
|
1614
|
+
handle_generic_xray
|
|
1615
|
+
return
|
|
1616
|
+
end
|
|
1617
|
+
|
|
1526
1618
|
choice = @go_ui.show_test_type_menu("xray")
|
|
1527
1619
|
|
|
1528
1620
|
case choice
|
|
@@ -1614,16 +1706,7 @@ class CLI
|
|
|
1614
1706
|
|
|
1615
1707
|
@go_ui.show_spinner("Tracing #{method_info[:name]}...") do
|
|
1616
1708
|
begin
|
|
1617
|
-
|
|
1618
|
-
path = File.expand_path(file_info[:path])
|
|
1619
|
-
|
|
1620
|
-
class_name = method_info[:class_name] || file_info[:name]
|
|
1621
|
-
trace_json = Tng::Analyzer::Context.analyze_symbolic_trace(
|
|
1622
|
-
project_root,
|
|
1623
|
-
path,
|
|
1624
|
-
method_info[:name],
|
|
1625
|
-
class_name
|
|
1626
|
-
)
|
|
1709
|
+
trace_json = fetch_trace_json(file_info, method_info, v2: false)
|
|
1627
1710
|
|
|
1628
1711
|
f = Tempfile.new(['trace', '.json'])
|
|
1629
1712
|
f.write(JSON.generate(trace_json))
|
|
@@ -1642,21 +1725,101 @@ class CLI
|
|
|
1642
1725
|
end
|
|
1643
1726
|
end
|
|
1644
1727
|
|
|
1645
|
-
def
|
|
1728
|
+
def run_trace_for_method_v2(file_info, method_info)
|
|
1646
1729
|
result_path = nil
|
|
1647
1730
|
|
|
1648
|
-
@go_ui.show_spinner("
|
|
1731
|
+
@go_ui.show_spinner("Tracing #{method_info[:name]} (v2)...") do
|
|
1649
1732
|
begin
|
|
1650
|
-
|
|
1651
|
-
path = File.expand_path(file_info[:path])
|
|
1733
|
+
trace_json = fetch_trace_json(file_info, method_info, v2: true)
|
|
1652
1734
|
|
|
1653
|
-
|
|
1654
|
-
|
|
1735
|
+
f = Tempfile.new(['trace_v2', '.json'])
|
|
1736
|
+
f.write(JSON.generate(trace_json))
|
|
1737
|
+
f.close
|
|
1738
|
+
result_path = f.path
|
|
1739
|
+
|
|
1740
|
+
{ success: true, message: "Trace v2 generated" }
|
|
1741
|
+
rescue => e
|
|
1742
|
+
{ success: false, message: e.message }
|
|
1743
|
+
end
|
|
1744
|
+
end
|
|
1745
|
+
|
|
1746
|
+
if result_path
|
|
1747
|
+
@go_ui.show_trace_results(result_path)
|
|
1748
|
+
File.unlink(result_path) if File.exist?(result_path)
|
|
1749
|
+
end
|
|
1750
|
+
end
|
|
1751
|
+
|
|
1752
|
+
def fetch_trace_json(file_info, method_info, v2:)
|
|
1753
|
+
project_root = Dir.pwd
|
|
1754
|
+
path = File.expand_path(file_info[:path])
|
|
1755
|
+
# For direct trace runs, avoid forcing a class name from file path.
|
|
1756
|
+
# In many codebases (e.g. service/agent folders), path-derived constants
|
|
1757
|
+
# do not match the real Ruby namespace and would hide valid methods.
|
|
1758
|
+
class_name = method_info[:class_name]
|
|
1759
|
+
|
|
1760
|
+
if @generic_mode
|
|
1761
|
+
if v2
|
|
1762
|
+
Tng::Analyzer::Context.analyze_symbolic_trace_ruby_v2(
|
|
1655
1763
|
project_root,
|
|
1656
1764
|
path,
|
|
1657
1765
|
method_info[:name],
|
|
1658
1766
|
class_name
|
|
1659
1767
|
)
|
|
1768
|
+
else
|
|
1769
|
+
Tng::Analyzer::Context.analyze_symbolic_trace_ruby(
|
|
1770
|
+
project_root,
|
|
1771
|
+
path,
|
|
1772
|
+
method_info[:name],
|
|
1773
|
+
class_name
|
|
1774
|
+
)
|
|
1775
|
+
end
|
|
1776
|
+
elsif v2
|
|
1777
|
+
Tng::Analyzer::Context.analyze_symbolic_trace_v2(
|
|
1778
|
+
project_root,
|
|
1779
|
+
path,
|
|
1780
|
+
method_info[:name],
|
|
1781
|
+
class_name
|
|
1782
|
+
)
|
|
1783
|
+
else
|
|
1784
|
+
Tng::Analyzer::Context.analyze_symbolic_trace(
|
|
1785
|
+
project_root,
|
|
1786
|
+
path,
|
|
1787
|
+
method_info[:name],
|
|
1788
|
+
class_name
|
|
1789
|
+
)
|
|
1790
|
+
end
|
|
1791
|
+
end
|
|
1792
|
+
|
|
1793
|
+
def run_impact_for_method(file_info, method_info)
|
|
1794
|
+
result_path = nil
|
|
1795
|
+
|
|
1796
|
+
@go_ui.show_spinner("Running regression check for #{method_info[:name]}...") do
|
|
1797
|
+
begin
|
|
1798
|
+
project_root = Dir.pwd
|
|
1799
|
+
path = File.expand_path(file_info[:path])
|
|
1800
|
+
|
|
1801
|
+
class_name =
|
|
1802
|
+
if @generic_mode
|
|
1803
|
+
method_info[:class_name]
|
|
1804
|
+
else
|
|
1805
|
+
method_info[:class_name] || file_info[:name]
|
|
1806
|
+
end
|
|
1807
|
+
impact_json =
|
|
1808
|
+
if @generic_mode
|
|
1809
|
+
Tng::Analyzer::Context.analyze_impact_ruby(
|
|
1810
|
+
project_root,
|
|
1811
|
+
path,
|
|
1812
|
+
method_info[:name],
|
|
1813
|
+
class_name
|
|
1814
|
+
)
|
|
1815
|
+
else
|
|
1816
|
+
Tng::Analyzer::Context.analyze_impact(
|
|
1817
|
+
project_root,
|
|
1818
|
+
path,
|
|
1819
|
+
method_info[:name],
|
|
1820
|
+
class_name
|
|
1821
|
+
)
|
|
1822
|
+
end
|
|
1660
1823
|
|
|
1661
1824
|
f = Tempfile.new(['impact', '.json'])
|
|
1662
1825
|
f.write(JSON.generate(impact_json))
|
|
@@ -1696,9 +1859,43 @@ class CLI
|
|
|
1696
1859
|
file_info = { path: full_path, name: namespaced_name }
|
|
1697
1860
|
method_info = { name: method_name, class_name: params[:class_name] }
|
|
1698
1861
|
|
|
1862
|
+
if params[:json]
|
|
1863
|
+
puts JSON.generate(fetch_trace_json(file_info, method_info, v2: false))
|
|
1864
|
+
return
|
|
1865
|
+
end
|
|
1866
|
+
|
|
1699
1867
|
run_trace_for_method(file_info, method_info)
|
|
1700
1868
|
end
|
|
1701
1869
|
|
|
1870
|
+
def run_direct_trace_v2
|
|
1871
|
+
file_path = params[:file]
|
|
1872
|
+
method_name = params[:method]
|
|
1873
|
+
|
|
1874
|
+
unless method_name
|
|
1875
|
+
puts @pastel.decorate("Please specify a method name with --method or -m", Tng::UI::Theme.color(:error))
|
|
1876
|
+
return
|
|
1877
|
+
end
|
|
1878
|
+
|
|
1879
|
+
full_path = File.expand_path(file_path)
|
|
1880
|
+
unless File.exist?(full_path)
|
|
1881
|
+
puts @pastel.decorate("File not found: #{full_path}", Tng::UI::Theme.color(:error))
|
|
1882
|
+
return
|
|
1883
|
+
end
|
|
1884
|
+
return unless ensure_not_ignored(full_path)
|
|
1885
|
+
|
|
1886
|
+
relative_path = full_path.gsub("#{Dir.pwd}/", "")
|
|
1887
|
+
namespaced_name = relative_path.sub(/\.rb\z/, "").split("/").map(&:camelize).join("::")
|
|
1888
|
+
file_info = { path: full_path, name: namespaced_name }
|
|
1889
|
+
method_info = { name: method_name, class_name: params[:class_name] }
|
|
1890
|
+
|
|
1891
|
+
if params[:json]
|
|
1892
|
+
puts JSON.generate(fetch_trace_json(file_info, method_info, v2: true))
|
|
1893
|
+
return
|
|
1894
|
+
end
|
|
1895
|
+
|
|
1896
|
+
run_trace_for_method_v2(file_info, method_info)
|
|
1897
|
+
end
|
|
1898
|
+
|
|
1702
1899
|
def run_direct_call_sites
|
|
1703
1900
|
file_path = params[:file]
|
|
1704
1901
|
method_name = params[:method]
|
|
@@ -1805,20 +2002,27 @@ class CLI
|
|
|
1805
2002
|
|
|
1806
2003
|
@go_ui.show_spinner("Generating X-Ray for #{method_info[:name]}...") do
|
|
1807
2004
|
begin
|
|
1808
|
-
type = Tng::Services::FileTypeDetector.detect_type(file_info[:path])
|
|
1809
|
-
file_object = Tng::Services::FileTypeDetector.build_file_object(file_info[:path], type)
|
|
1810
2005
|
generator = Tng::Services::TestGenerator.new(@http_client)
|
|
1811
2006
|
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2007
|
+
if @generic_mode
|
|
2008
|
+
method_info = method_info.merge(test_type: method_info[:test_type] || "other")
|
|
2009
|
+
file_object = generic_file_object(file_info)
|
|
2010
|
+
xray_result = generator.run_xray_for_ruby_method(file_object, method_info)
|
|
2011
|
+
else
|
|
2012
|
+
type = Tng::Services::FileTypeDetector.detect_type(file_info[:path])
|
|
2013
|
+
file_object = Tng::Services::FileTypeDetector.build_file_object(file_info[:path], type)
|
|
2014
|
+
|
|
2015
|
+
xray_result = case type
|
|
2016
|
+
when "controller"
|
|
2017
|
+
generator.run_xray_for_controller_method(file_object, method_info)
|
|
2018
|
+
when "model"
|
|
2019
|
+
generator.run_xray_for_model_method(file_object, method_info)
|
|
2020
|
+
when "service"
|
|
2021
|
+
generator.run_xray_for_service_method(file_object, method_info)
|
|
2022
|
+
else
|
|
2023
|
+
generator.run_xray_for_other_method(file_object, method_info)
|
|
2024
|
+
end
|
|
2025
|
+
end
|
|
1822
2026
|
|
|
1823
2027
|
if xray_result&.dig(:error)
|
|
1824
2028
|
{ success: false, message: xray_result[:message] || "X-Ray failed" }
|
|
@@ -1996,11 +2200,196 @@ class CLI
|
|
|
1996
2200
|
next unless m_choice
|
|
1997
2201
|
m_choice = m_choice.merge(class_name: selected_class || choice[:name])
|
|
1998
2202
|
|
|
1999
|
-
|
|
2203
|
+
yield(choice, m_choice)
|
|
2204
|
+
end
|
|
2205
|
+
end
|
|
2206
|
+
|
|
2207
|
+
def generic_file_object(file_info)
|
|
2208
|
+
name = file_info[:name].sub(/\.rb\z/, "")
|
|
2209
|
+
{
|
|
2210
|
+
name: name.split("/").map(&:camelize).join("::"),
|
|
2211
|
+
path: file_info[:path],
|
|
2212
|
+
type: "other"
|
|
2213
|
+
}
|
|
2214
|
+
end
|
|
2215
|
+
|
|
2216
|
+
def list_generic_files
|
|
2217
|
+
root = Dir.pwd
|
|
2218
|
+
files = Tng::Services::FileDiscovery.list_ruby_files(root)
|
|
2219
|
+
files.map do |path|
|
|
2220
|
+
{
|
|
2221
|
+
name: path.gsub(%r{\A#{Regexp.escape(root)}/?}, ""),
|
|
2222
|
+
path: path
|
|
2223
|
+
}
|
|
2224
|
+
end
|
|
2225
|
+
end
|
|
2226
|
+
|
|
2227
|
+
def select_generic_file(action_name)
|
|
2228
|
+
files = nil
|
|
2229
|
+
@go_ui.show_spinner("Analyzing files...") do
|
|
2230
|
+
files = list_generic_files
|
|
2231
|
+
{ success: true, message: "Found #{files.length} files" }
|
|
2232
|
+
end
|
|
2233
|
+
|
|
2234
|
+
return @go_ui.show_no_items("ruby files") if files.empty?
|
|
2235
|
+
|
|
2236
|
+
items = files.map { |f| { name: f[:name], path: f[:path] } }
|
|
2237
|
+
selected = @go_ui.show_list_view("Select File to #{action_name}", items)
|
|
2238
|
+
return if selected == "back"
|
|
2239
|
+
|
|
2240
|
+
files.find { |f| f[:name] == selected }
|
|
2241
|
+
end
|
|
2242
|
+
|
|
2243
|
+
def select_generic_method(file_info, action_name)
|
|
2244
|
+
file_object = generic_file_object(file_info)
|
|
2245
|
+
methods = extract_other_methods(file_object)
|
|
2246
|
+
if methods.empty?
|
|
2247
|
+
@go_ui.show_no_items("methods in #{file_info[:name]}")
|
|
2248
|
+
return
|
|
2249
|
+
end
|
|
2250
|
+
|
|
2251
|
+
items = methods.map { |m| { name: m[:name], path: file_info[:name] } }
|
|
2252
|
+
selected = @go_ui.show_list_view("Select Method to #{action_name}", items)
|
|
2253
|
+
return if selected == "back"
|
|
2254
|
+
|
|
2255
|
+
methods.find { |m| m[:name] == selected }
|
|
2256
|
+
end
|
|
2257
|
+
|
|
2258
|
+
def select_generic_method_and_type(action_name)
|
|
2259
|
+
file_info = select_generic_file(action_name)
|
|
2260
|
+
return if file_info.nil?
|
|
2261
|
+
|
|
2262
|
+
method_info = select_generic_method(file_info, action_name)
|
|
2263
|
+
return if method_info.nil?
|
|
2264
|
+
|
|
2265
|
+
test_type = @go_ui.show_ruby_test_type_menu
|
|
2266
|
+
return if test_type == "back"
|
|
2267
|
+
|
|
2268
|
+
[file_info, method_info.merge(test_type: test_type)]
|
|
2269
|
+
end
|
|
2270
|
+
|
|
2271
|
+
def handle_generic_test_generation
|
|
2272
|
+
selection = select_generic_method_and_type("Generate Tests")
|
|
2273
|
+
return if selection.nil?
|
|
2274
|
+
|
|
2275
|
+
file_info, method_info = selection
|
|
2276
|
+
file_object = generic_file_object(file_info)
|
|
2277
|
+
|
|
2278
|
+
@go_ui.display_info(@pastel.bright_white("🎯 Generating test for #{file_info[:name]}##{method_info[:name]}..."))
|
|
2279
|
+
|
|
2280
|
+
result = @go_ui.show_progress("Processing...") do |progress|
|
|
2281
|
+
Tng::Services::TestGenerator.new(@http_client).run_for_ruby_method(
|
|
2282
|
+
file_object,
|
|
2283
|
+
method_info,
|
|
2284
|
+
progress: progress
|
|
2285
|
+
)
|
|
2286
|
+
end
|
|
2287
|
+
|
|
2288
|
+
if result&.dig(:error)
|
|
2289
|
+
@go_ui.display_error(@pastel.red("❌ Test generation failed: #{result[:message]}"))
|
|
2290
|
+
return
|
|
2291
|
+
end
|
|
2292
|
+
|
|
2293
|
+
show_post_generation_menu(result) if result && result[:file_path]
|
|
2294
|
+
end
|
|
2295
|
+
|
|
2296
|
+
def handle_generic_audit
|
|
2297
|
+
selection = select_generic_method_and_type("Audit")
|
|
2298
|
+
return if selection.nil?
|
|
2299
|
+
|
|
2300
|
+
file_info, method_info = selection
|
|
2301
|
+
file_object = generic_file_object(file_info)
|
|
2302
|
+
|
|
2303
|
+
audit_result = nil
|
|
2304
|
+
result = @go_ui.show_progress("Auditing #{file_info[:name]}##{method_info[:name]}") do |progress|
|
|
2305
|
+
progress.update("Analyzing method context...", 25)
|
|
2306
|
+
progress.update("Running logical analysis...", 50)
|
|
2307
|
+
progress.update("Detecting issues and behaviours...", 75)
|
|
2308
|
+
|
|
2309
|
+
audit_result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_ruby_method(
|
|
2310
|
+
file_object,
|
|
2311
|
+
method_info,
|
|
2312
|
+
progress: progress
|
|
2313
|
+
)
|
|
2314
|
+
|
|
2315
|
+
progress.update("Processing results...", 100)
|
|
2316
|
+
audit_result&.dig(:error) ? { message: audit_result[:message] || "Audit failed", error: audit_result[:error] } : { message: "Audit complete!" }
|
|
2317
|
+
end
|
|
2318
|
+
|
|
2319
|
+
return @go_ui.show_auth_error(audit_result[:message] || "Audit failed") if audit_result&.dig(:error)
|
|
2320
|
+
|
|
2321
|
+
display_audit_results(audit_result || result)
|
|
2322
|
+
end
|
|
2323
|
+
|
|
2324
|
+
def handle_generic_xray
|
|
2325
|
+
selection = select_generic_method_and_type("X-Ray")
|
|
2326
|
+
return if selection.nil?
|
|
2327
|
+
|
|
2328
|
+
file_info, method_info = selection
|
|
2329
|
+
file_object = generic_file_object(file_info)
|
|
2330
|
+
|
|
2331
|
+
xray_result = nil
|
|
2332
|
+
@go_ui.show_spinner("Generating X-Ray for #{method_info[:name]}...") do
|
|
2333
|
+
xray_result = Tng::Services::TestGenerator.new(@http_client).run_xray_for_ruby_method(
|
|
2334
|
+
file_object,
|
|
2335
|
+
method_info
|
|
2336
|
+
)
|
|
2337
|
+
|
|
2338
|
+
if xray_result&.dig(:error)
|
|
2339
|
+
{ success: false, message: xray_result[:message] || "X-Ray failed" }
|
|
2340
|
+
else
|
|
2341
|
+
{ success: true, message: "X-Ray generated" }
|
|
2000
2342
|
end
|
|
2343
|
+
end
|
|
2344
|
+
|
|
2345
|
+
return @go_ui.display_error(@pastel.red("❌ X-Ray failed: #{xray_result[:message]}")) if xray_result&.dig(:error)
|
|
2346
|
+
|
|
2347
|
+
@go_ui.show_xray_results(xray_result, method_info) if xray_result
|
|
2348
|
+
end
|
|
2349
|
+
|
|
2350
|
+
def handle_generic_trace
|
|
2351
|
+
file_info = select_generic_file("Trace")
|
|
2352
|
+
return if file_info.nil?
|
|
2353
|
+
|
|
2354
|
+
method_info = select_generic_method(file_info, "Trace")
|
|
2355
|
+
return if method_info.nil?
|
|
2356
|
+
|
|
2357
|
+
run_trace_for_method(file_info, method_info)
|
|
2358
|
+
end
|
|
2359
|
+
|
|
2360
|
+
def handle_generic_impact
|
|
2361
|
+
file_info = select_generic_file("Regression Check")
|
|
2362
|
+
return if file_info.nil?
|
|
2363
|
+
|
|
2364
|
+
method_info = select_generic_method(file_info, "Regression Check")
|
|
2365
|
+
return if method_info.nil?
|
|
2366
|
+
|
|
2367
|
+
run_impact_for_method(file_info, method_info)
|
|
2368
|
+
end
|
|
2369
|
+
|
|
2370
|
+
def handle_generic_clone_detection
|
|
2371
|
+
file_info = select_generic_file("Check Duplicates")
|
|
2372
|
+
return if file_info.nil?
|
|
2373
|
+
|
|
2374
|
+
level = @go_ui.show_clone_menu
|
|
2375
|
+
return if level == "back"
|
|
2376
|
+
|
|
2377
|
+
run_clone_detection_for_file(file_info, level)
|
|
2378
|
+
end
|
|
2379
|
+
|
|
2380
|
+
def handle_generic_dead_code
|
|
2381
|
+
file_info = select_generic_file("Analyze Dead Code")
|
|
2382
|
+
return if file_info.nil?
|
|
2383
|
+
|
|
2384
|
+
run_dead_code_detection_interactive(file_info[:path])
|
|
2001
2385
|
end
|
|
2002
2386
|
|
|
2003
2387
|
def handle_dead_code_detection_menu
|
|
2388
|
+
if @generic_mode
|
|
2389
|
+
handle_generic_dead_code
|
|
2390
|
+
return
|
|
2391
|
+
end
|
|
2392
|
+
|
|
2004
2393
|
# Pick a file type first to narrow down
|
|
2005
2394
|
choice = @go_ui.show_test_type_menu("deadcode")
|
|
2006
2395
|
return if choice == "back"
|