tng 0.5.2 → 0.5.4
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 +4 -4
- data/bin/tng +158 -42
- 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 +4 -0
- data/lib/tng/analyzers/controller.rb +17 -2
- data/lib/tng/analyzers/model.rb +5 -3
- data/lib/tng/analyzers/other.rb +1 -1
- data/lib/tng/analyzers/service.rb +12 -10
- data/lib/tng/api/http_client.rb +4 -4
- data/lib/tng/services/direct_generation.rb +19 -1
- data/lib/tng/services/extract_methods.rb +3 -3
- data/lib/tng/services/file_type_detector.rb +50 -14
- data/lib/tng/services/test_generator.rb +67 -78
- data/lib/tng/ui/go_ui_session.rb +4 -4
- data/lib/tng/ui/post_install_box.rb +12 -12
- data/lib/tng/ui/theme.rb +22 -0
- data/lib/tng/utils.rb +63 -0
- data/lib/tng/version.rb +1 -1
- data/lib/tng.rb +19 -1
- metadata +5 -26
|
@@ -24,7 +24,7 @@ module Tng
|
|
|
24
24
|
Tng::Analyzer::Service.parse_service_file(file_path)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def self.methods_for_service(service_name)
|
|
27
|
+
def self.methods_for_service(service_name, file_path = nil)
|
|
28
28
|
raise "service_name is required" if service_name.nil?
|
|
29
29
|
|
|
30
30
|
begin
|
|
@@ -35,11 +35,12 @@ module Tng
|
|
|
35
35
|
service_class.private_instance_methods(false)
|
|
36
36
|
class_methods = service_class.public_methods(false) - Class.public_methods
|
|
37
37
|
|
|
38
|
-
#
|
|
39
|
-
service_file =
|
|
38
|
+
# Prefer explicit file path or class definition location
|
|
39
|
+
service_file = file_path
|
|
40
|
+
service_file ||= Object.const_source_location(service_class.name)&.first
|
|
40
41
|
|
|
41
|
-
#
|
|
42
|
-
if instance_methods.any?
|
|
42
|
+
# Fallback to any instance method source location
|
|
43
|
+
if service_file.nil? && instance_methods.any?
|
|
43
44
|
begin
|
|
44
45
|
service_file = service_class.instance_method(instance_methods.first).source_location&.first
|
|
45
46
|
rescue StandardError
|
|
@@ -47,9 +48,6 @@ module Tng
|
|
|
47
48
|
end
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
# Fallback to const_source_location if no method source found
|
|
51
|
-
service_file ||= Object.const_source_location(service_class.name)&.first
|
|
52
|
-
|
|
53
51
|
service_methods = if service_file && File.exist?(service_file)
|
|
54
52
|
source_code = File.read(service_file)
|
|
55
53
|
result = Prism.parse(source_code)
|
|
@@ -68,7 +66,11 @@ module Tng
|
|
|
68
66
|
defined_methods.include?(method_name.to_s)
|
|
69
67
|
end
|
|
70
68
|
|
|
71
|
-
filtered_instance_methods
|
|
69
|
+
if filtered_instance_methods.empty? && filtered_class_methods.empty? && defined_methods.any?
|
|
70
|
+
defined_methods
|
|
71
|
+
else
|
|
72
|
+
filtered_instance_methods + filtered_class_methods
|
|
73
|
+
end
|
|
72
74
|
else
|
|
73
75
|
[]
|
|
74
76
|
end
|
|
@@ -107,7 +109,7 @@ module Tng
|
|
|
107
109
|
path: file_path,
|
|
108
110
|
relative_path: relative_path
|
|
109
111
|
}
|
|
110
|
-
end
|
|
112
|
+
end.reject { |file| Tng::Utils.ignored_path?(file[:path]) }
|
|
111
113
|
end
|
|
112
114
|
end
|
|
113
115
|
end
|
data/lib/tng/api/http_client.rb
CHANGED
|
@@ -6,10 +6,10 @@ module Tng
|
|
|
6
6
|
@api_endpoint = api_endpoint
|
|
7
7
|
@api_key = api_key
|
|
8
8
|
@timeout = {
|
|
9
|
-
connect_timeout:
|
|
10
|
-
read_timeout:
|
|
11
|
-
write_timeout:
|
|
12
|
-
request_timeout:
|
|
9
|
+
connect_timeout: 420,
|
|
10
|
+
read_timeout: 420,
|
|
11
|
+
write_timeout: 420,
|
|
12
|
+
request_timeout: 420
|
|
13
13
|
}
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -35,13 +35,27 @@ module Tng
|
|
|
35
35
|
return
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
if Tng::Utils.ignored_path?(file_path)
|
|
39
|
+
@go_ui.display_error(@pastel.red("❌ Ignored by TNG config (ignore_files/ignore_folders). Remove it from config to proceed."))
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
resolved_result = FileTypeDetector.resolve_file_path(file_path)
|
|
44
|
+
if resolved_result.is_a?(Hash) && resolved_result[:ignored]
|
|
45
|
+
@go_ui.display_error(@pastel.red("❌ Ignored by TNG config (ignore_files/ignore_folders). Remove it from config to proceed."))
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
resolved_path = resolved_result.is_a?(Hash) ? resolved_result[:path] : resolved_result
|
|
39
49
|
|
|
40
50
|
unless resolved_path && File.exist?(resolved_path)
|
|
41
51
|
@go_ui.display_error(@pastel.red("❌ File not found: #{file_path}"))
|
|
42
52
|
suggest_similar_files(file_path)
|
|
43
53
|
return
|
|
44
54
|
end
|
|
55
|
+
if Tng::Utils.ignored_path?(resolved_path)
|
|
56
|
+
@go_ui.display_error(@pastel.red("❌ Ignored by TNG config (ignore_files/ignore_folders). Remove it from config to proceed."))
|
|
57
|
+
return
|
|
58
|
+
end
|
|
45
59
|
|
|
46
60
|
type = FileTypeDetector.detect_type(resolved_path)
|
|
47
61
|
|
|
@@ -72,6 +86,7 @@ module Tng
|
|
|
72
86
|
next unless Dir.exist?(File.join(rails_root, dir))
|
|
73
87
|
|
|
74
88
|
Dir.glob(File.join(rails_root, dir, "**", "*#{base_name}*.rb")).each do |file|
|
|
89
|
+
next if Tng::Utils.ignored_path?(file)
|
|
75
90
|
similar_files << file.gsub(%r{^#{Regexp.escape(rails_root)}/}, "")
|
|
76
91
|
end
|
|
77
92
|
end
|
|
@@ -89,6 +104,9 @@ module Tng
|
|
|
89
104
|
end
|
|
90
105
|
|
|
91
106
|
method_info = methods.find { |m| m[:name].downcase == method_name.downcase }
|
|
107
|
+
if method_info && @params[:class_name]
|
|
108
|
+
method_info = method_info.merge(class_name: @params[:class_name])
|
|
109
|
+
end
|
|
92
110
|
|
|
93
111
|
unless method_info
|
|
94
112
|
@go_ui.display_error(@pastel.red("❌ Method '#{method_name}' not found in #{file_object[:name]}"))
|
|
@@ -4,21 +4,21 @@ module Tng
|
|
|
4
4
|
module Services
|
|
5
5
|
module ExtractMethods
|
|
6
6
|
def extract_controller_methods(controller)
|
|
7
|
-
Tng::Analyzers::Controller.methods_for_controller(controller[:name]) || []
|
|
7
|
+
Tng::Analyzers::Controller.methods_for_controller(controller[:name], controller[:path]) || []
|
|
8
8
|
rescue StandardError => e
|
|
9
9
|
puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing controller: #{e.message}", Tng::UI::Theme.color(:error)))
|
|
10
10
|
[]
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def extract_model_methods(model)
|
|
14
|
-
Tng::Analyzers::Model.methods_for_model(model[:name]) || []
|
|
14
|
+
Tng::Analyzers::Model.methods_for_model(model[:name], model[:path]) || []
|
|
15
15
|
rescue StandardError => e
|
|
16
16
|
puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing model: #{e.message}", Tng::UI::Theme.color(:error)))
|
|
17
17
|
[]
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def extract_service_methods(service)
|
|
21
|
-
Tng::Analyzers::Service.methods_for_service(service[:name]) || []
|
|
21
|
+
Tng::Analyzers::Service.methods_for_service(service[:name], service[:path]) || []
|
|
22
22
|
rescue StandardError => e
|
|
23
23
|
puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:error)} Error analyzing service: #{e.message}", Tng::UI::Theme.color(:error)))
|
|
24
24
|
[]
|
|
@@ -70,33 +70,69 @@ module Tng
|
|
|
70
70
|
lib app/lib
|
|
71
71
|
].freeze
|
|
72
72
|
|
|
73
|
-
def
|
|
73
|
+
def candidate_paths_for(file_name)
|
|
74
74
|
file_with_ext = file_name.end_with?('.rb') ? file_name : "#{file_name}.rb"
|
|
75
|
+
candidates = []
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
if File.exist?(file_with_ext)
|
|
78
|
+
candidates << File.expand_path(file_with_ext)
|
|
79
|
+
end
|
|
77
80
|
|
|
78
81
|
rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
|
|
79
82
|
|
|
80
83
|
SEARCH_PATHS.each do |dir|
|
|
81
84
|
full_path = File.join(rails_root, dir, file_with_ext)
|
|
82
|
-
|
|
85
|
+
candidates << full_path if File.exist?(full_path)
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
return found_files.first unless found_files.empty?
|
|
87
|
+
candidates.concat(Dir.glob(File.join(rails_root, dir, '**', file_with_ext)))
|
|
86
88
|
end
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
candidates.uniq
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def find_file_in_project(file_name)
|
|
94
|
+
candidate_paths_for(file_name).find { |candidate| !Tng::Utils.ignored_path?(candidate) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def find_ignored_in_project(file_name)
|
|
98
|
+
candidate_paths_for(file_name).find { |candidate| Tng::Utils.ignored_path?(candidate) }
|
|
89
99
|
end
|
|
90
100
|
|
|
91
101
|
def resolve_file_path(file_path)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
if file_path.start_with?('/')
|
|
103
|
+
candidates = []
|
|
104
|
+
candidates << file_path if File.exist?(file_path)
|
|
105
|
+
candidates << "#{file_path}.rb" if File.exist?("#{file_path}.rb")
|
|
106
|
+
|
|
107
|
+
ignored_candidate = nil
|
|
108
|
+
candidates.each do |candidate|
|
|
109
|
+
expanded = File.expand_path(candidate)
|
|
110
|
+
if Tng::Utils.ignored_path?(expanded)
|
|
111
|
+
ignored_candidate ||= expanded
|
|
112
|
+
next
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
return { path: expanded, ignored: false }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
return { path: ignored_candidate, ignored: true } if ignored_candidate
|
|
119
|
+
return nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
ignored_candidate = nil
|
|
123
|
+
candidate_paths_for(file_path).each do |candidate|
|
|
124
|
+
expanded = File.expand_path(candidate)
|
|
125
|
+
if Tng::Utils.ignored_path?(expanded)
|
|
126
|
+
ignored_candidate ||= expanded
|
|
127
|
+
next
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
return { path: expanded, ignored: false }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
return { path: ignored_candidate, ignored: true } if ignored_candidate
|
|
134
|
+
|
|
135
|
+
nil
|
|
100
136
|
end
|
|
101
137
|
|
|
102
138
|
def extract_relative_path(file_path, type)
|
|
@@ -89,7 +89,7 @@ module Tng
|
|
|
89
89
|
job_id = job_data["job_id"]
|
|
90
90
|
return { error: :server_error, message: "No job_id returned" } unless job_id
|
|
91
91
|
|
|
92
|
-
# Poll for completion (
|
|
92
|
+
# Poll for completion (audit only)
|
|
93
93
|
result = poll_for_completion(job_id, progress: progress)
|
|
94
94
|
return { error: :timeout, message: "Audit timed out" } unless result
|
|
95
95
|
|
|
@@ -234,33 +234,7 @@ module Tng
|
|
|
234
234
|
max_attempts = MAX_POLL_DURATION_SECONDS / POLL_INTERVAL_SECONDS
|
|
235
235
|
|
|
236
236
|
# Initialize agent steps if progress tracking is enabled
|
|
237
|
-
agent_step_indices =
|
|
238
|
-
if progress
|
|
239
|
-
# Current step index in progress updater (assuming it follows previous steps)
|
|
240
|
-
# We create 4 generic steps for the agents
|
|
241
|
-
# Note: indices are relative to the current session, so we just append them.
|
|
242
|
-
# But we need their indices to update them later.
|
|
243
|
-
# Since we can't ask progress for current index easily without hacking,
|
|
244
|
-
# we rely on the fact that we call update 4 times.
|
|
245
|
-
|
|
246
|
-
# Use a base offset if we could know it, but we can't reliably.
|
|
247
|
-
# Actually, if we use explicit_step, we need absolute indices.
|
|
248
|
-
# Let's assume the previous steps were 0, 1, 2, 3 based on bin/tng.
|
|
249
|
-
# So we start at 4.
|
|
250
|
-
base_idx = 4
|
|
251
|
-
|
|
252
|
-
progress.update("Context Builder: Pending...", nil, step_increment: true)
|
|
253
|
-
agent_step_indices["context_agent_status"] = base_idx
|
|
254
|
-
|
|
255
|
-
progress.update("Style Analyzer: Pending...", nil, step_increment: true)
|
|
256
|
-
agent_step_indices["style_agent_status"] = base_idx + 1
|
|
257
|
-
|
|
258
|
-
progress.update("Logic Analyzer: Pending...", nil, step_increment: true)
|
|
259
|
-
agent_step_indices["logical_issue_status"] = base_idx + 2
|
|
260
|
-
|
|
261
|
-
progress.update("Logic Generator: Pending...", nil, step_increment: true)
|
|
262
|
-
agent_step_indices["behavior_expert_status"] = base_idx + 3
|
|
263
|
-
end
|
|
237
|
+
agent_step_indices = init_agent_steps(progress)
|
|
264
238
|
|
|
265
239
|
loop do
|
|
266
240
|
attempts += 1
|
|
@@ -294,56 +268,7 @@ module Tng
|
|
|
294
268
|
status = status_data[:status]
|
|
295
269
|
|
|
296
270
|
# Update UI with granular info
|
|
297
|
-
|
|
298
|
-
info = status_data[:info]
|
|
299
|
-
|
|
300
|
-
agent_step_indices.each do |key, step_idx|
|
|
301
|
-
item_data = info[key.to_sym]
|
|
302
|
-
next unless item_data
|
|
303
|
-
|
|
304
|
-
agent_status = "pending"
|
|
305
|
-
values = []
|
|
306
|
-
|
|
307
|
-
if item_data.is_a?(Hash)
|
|
308
|
-
agent_status = item_data[:status] || "pending"
|
|
309
|
-
values = item_data[:values] || []
|
|
310
|
-
else
|
|
311
|
-
agent_status = item_data.to_s
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
label = case key
|
|
315
|
-
when "context_agent_status" then "Context Builder"
|
|
316
|
-
when "style_agent_status" then "Style Analyzer"
|
|
317
|
-
when "logical_issue_status" then "Logic Analyzer"
|
|
318
|
-
when "behavior_expert_status" then "Logic Generator"
|
|
319
|
-
else key
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
msg = if agent_status == "processing"
|
|
323
|
-
"#{label}: Processing..."
|
|
324
|
-
elsif agent_status == "completed"
|
|
325
|
-
"#{label}: Completed"
|
|
326
|
-
elsif agent_status == "failed"
|
|
327
|
-
"#{label}: Failed"
|
|
328
|
-
else
|
|
329
|
-
"#{label}: #{agent_status.capitalize}..."
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
if values.any?
|
|
333
|
-
# Clean values
|
|
334
|
-
clean_vals = values.map { |v| v.to_s.gsub("_", " ").gsub("'", "").gsub(":", "").strip }
|
|
335
|
-
display_str = clean_vals.first(3).join(", ")
|
|
336
|
-
display_str += ", ..." if clean_vals.size > 3
|
|
337
|
-
msg += " (#{display_str})"
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
# Pass percentage only on the last step (Logic Generator) to keep main bar moving?
|
|
341
|
-
# Or pass it on all updates.
|
|
342
|
-
p = step_idx == agent_step_indices["behavior_expert_status"] ? pct : nil
|
|
343
|
-
|
|
344
|
-
progress.update(msg, p, step_increment: false, explicit_step: step_idx)
|
|
345
|
-
end
|
|
346
|
-
end
|
|
271
|
+
update_progress_from_info(progress, agent_step_indices, status_data[:info], pct) if progress
|
|
347
272
|
|
|
348
273
|
case status
|
|
349
274
|
when "completed"
|
|
@@ -381,6 +306,70 @@ module Tng
|
|
|
381
306
|
exit(0)
|
|
382
307
|
end
|
|
383
308
|
|
|
309
|
+
def init_agent_steps(progress)
|
|
310
|
+
return {} unless progress
|
|
311
|
+
|
|
312
|
+
base_idx = 4
|
|
313
|
+
progress.update("Context Builder: Pending...", nil, step_increment: true)
|
|
314
|
+
progress.update("Style Analyzer: Pending...", nil, step_increment: true)
|
|
315
|
+
progress.update("Logic Analyzer: Pending...", nil, step_increment: true)
|
|
316
|
+
progress.update("Logic Generator: Pending...", nil, step_increment: true)
|
|
317
|
+
|
|
318
|
+
{
|
|
319
|
+
"context_agent_status" => base_idx,
|
|
320
|
+
"style_agent_status" => base_idx + 1,
|
|
321
|
+
"logical_issue_status" => base_idx + 2,
|
|
322
|
+
"behavior_expert_status" => base_idx + 3
|
|
323
|
+
}
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def update_progress_from_info(progress, agent_step_indices, info, percent)
|
|
327
|
+
return unless progress && info.is_a?(Hash)
|
|
328
|
+
|
|
329
|
+
agent_step_indices.each do |key, step_idx|
|
|
330
|
+
item_data = info[key.to_s] || info[key.to_sym]
|
|
331
|
+
next unless item_data
|
|
332
|
+
|
|
333
|
+
agent_status = "pending"
|
|
334
|
+
values = []
|
|
335
|
+
|
|
336
|
+
if item_data.is_a?(Hash)
|
|
337
|
+
agent_status = item_data[:status] || item_data["status"] || "pending"
|
|
338
|
+
values = item_data[:values] || item_data["values"] || []
|
|
339
|
+
else
|
|
340
|
+
agent_status = item_data.to_s
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
label = case key
|
|
344
|
+
when "context_agent_status" then "Context Builder"
|
|
345
|
+
when "style_agent_status" then "Style Analyzer"
|
|
346
|
+
when "logical_issue_status" then "Logic Analyzer"
|
|
347
|
+
when "behavior_expert_status" then "Logic Generator"
|
|
348
|
+
else key
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
msg = if agent_status == "processing"
|
|
352
|
+
"#{label}: Processing..."
|
|
353
|
+
elsif agent_status == "completed"
|
|
354
|
+
"#{label}: Completed"
|
|
355
|
+
elsif agent_status == "failed"
|
|
356
|
+
"#{label}: Failed"
|
|
357
|
+
else
|
|
358
|
+
"#{label}: #{agent_status.capitalize}..."
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
if values.any?
|
|
362
|
+
clean_vals = values.map { |v| v.to_s.gsub("_", " ").gsub("'", "").gsub(":", "").strip }
|
|
363
|
+
display_str = clean_vals.first(3).join(", ")
|
|
364
|
+
display_str += ", ..." if clean_vals.size > 3
|
|
365
|
+
msg += " (#{display_str})"
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
p = (percent && step_idx == agent_step_indices["behavior_expert_status"]) ? percent : nil
|
|
369
|
+
progress.update(msg, p, step_increment: false, explicit_step: step_idx)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
384
373
|
def trigger_cleanup(job_id)
|
|
385
374
|
@http_client.patch("#{CONTENT_RESPONSES_PATH}/#{job_id}/cleanup")
|
|
386
375
|
rescue StandardError => e
|
data/lib/tng/ui/go_ui_session.rb
CHANGED
|
@@ -256,7 +256,7 @@ module Tng
|
|
|
256
256
|
puts "Trace results error: #{e.message}"
|
|
257
257
|
end
|
|
258
258
|
|
|
259
|
-
# Show
|
|
259
|
+
# Show regression check results
|
|
260
260
|
# @param impact_file_path [String] Path to JSON impact file
|
|
261
261
|
def show_impact_results(impact_file_path)
|
|
262
262
|
system(@binary_path, "ruby-impact-results", "--file", impact_file_path)
|
|
@@ -415,7 +415,7 @@ module Tng
|
|
|
415
415
|
{ cmd: "bundle exec tng --file=FILE --method=METHOD", desc: "Direct test generation" },
|
|
416
416
|
{ cmd: "bundle exec tng --file=FILE --method=METHOD --audit", desc: "Direct audit mode" },
|
|
417
417
|
{ cmd: "bundle exec tng --file=FILE --method=METHOD --trace", desc: "Symbolic trace mode" },
|
|
418
|
-
{ cmd: "bundle exec tng --file=FILE --method=METHOD --impact", desc: "
|
|
418
|
+
{ cmd: "bundle exec tng --file=FILE --method=METHOD --impact", desc: "Regression check mode" },
|
|
419
419
|
{ cmd: "bundle exec tng --file=FILE --clones", desc: "Check for code duplicates" },
|
|
420
420
|
{ cmd: "bundle exec tng --file=FILE --deadcode", desc: "Run dead code detection" },
|
|
421
421
|
{ cmd: "bundle exec tng --file=FILE --method=METHOD --xray", desc: "X-Ray visualization" }
|
|
@@ -426,7 +426,7 @@ module Tng
|
|
|
426
426
|
"Per-method test generation",
|
|
427
427
|
"Code audit for issues & behaviors",
|
|
428
428
|
"Symbolic execution traces",
|
|
429
|
-
"
|
|
429
|
+
"Regression check against Git HEAD",
|
|
430
430
|
"Duplicate code detection (Clones)",
|
|
431
431
|
"Dead code detection (Rust-powered)",
|
|
432
432
|
"X-Ray Logic Flow Visualization",
|
|
@@ -438,7 +438,7 @@ module Tng
|
|
|
438
438
|
{ flag: "--method=NAME", desc: "Method name to generate test for" },
|
|
439
439
|
{ flag: "--audit", desc: "Run audit mode instead of test generation" },
|
|
440
440
|
{ flag: "--trace", desc: "Run symbolic trace mode" },
|
|
441
|
-
{ flag: "--impact", desc: "Run
|
|
441
|
+
{ flag: "--impact", desc: "Run regression check mode" },
|
|
442
442
|
{ flag: "--clones", desc: "Run clone detection mode" },
|
|
443
443
|
{ flag: "--level=1|2|3|all", desc: "Set clone detection level (default: all)" },
|
|
444
444
|
{ flag: "--deadcode", desc: "Run dead code detection" },
|
|
@@ -17,9 +17,9 @@ class PostInstallBox
|
|
|
17
17
|
|
|
18
18
|
def content
|
|
19
19
|
[
|
|
20
|
-
pastel.public_send(Tng::UI::Theme.color(:success)).bold("#{Tng::UI::Theme.icon(:success)} Tng installed successfully!"),
|
|
20
|
+
pastel.public_send(Tng::UI::Theme.color(:success)).bold("#{Tng::UI::Theme.icon(:success, ascii: true)} Tng installed successfully!"),
|
|
21
21
|
"",
|
|
22
|
-
pastel.public_send(Tng::UI::Theme.color(:warning)).bold("#{Tng::UI::Theme.icon(:config)} SETUP REQUIRED"),
|
|
22
|
+
pastel.public_send(Tng::UI::Theme.color(:warning)).bold("#{Tng::UI::Theme.icon(:config, ascii: true)} SETUP REQUIRED"),
|
|
23
23
|
"",
|
|
24
24
|
pastel.public_send(Tng::UI::Theme.color(:primary), "1. Generate configuration:"),
|
|
25
25
|
pastel.public_send(Tng::UI::Theme.color(:secondary), " rails g tng:install"),
|
|
@@ -31,30 +31,30 @@ class PostInstallBox
|
|
|
31
31
|
pastel.public_send(Tng::UI::Theme.color(:secondary), " config.api_key = 'your-license-key-here'"),
|
|
32
32
|
"",
|
|
33
33
|
pastel.public_send(Tng::UI::Theme.color(:primary),
|
|
34
|
-
"#{Tng::UI::Theme.icon(:config)} Check documentation for the correct authentication setup"),
|
|
34
|
+
"#{Tng::UI::Theme.icon(:config, ascii: true)} Check documentation for the correct authentication setup"),
|
|
35
35
|
"",
|
|
36
|
-
pastel.public_send(Tng::UI::Theme.color(:accent)).bold("#{Tng::UI::Theme.icon(:rocket)} Usage:"),
|
|
36
|
+
pastel.public_send(Tng::UI::Theme.color(:accent)).bold("#{Tng::UI::Theme.icon(:rocket, ascii: true)} Usage:"),
|
|
37
37
|
"",
|
|
38
38
|
pastel.public_send(Tng::UI::Theme.color(:primary), "Interactive mode:"),
|
|
39
39
|
pastel.public_send(Tng::UI::Theme.color(:success),
|
|
40
|
-
"#{Tng::UI::Theme.icon(:bullet)} bundle exec tng") + pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
41
|
-
|
|
40
|
+
"#{Tng::UI::Theme.icon(:bullet, ascii: true)} bundle exec tng") + pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
41
|
+
" - Interactive CLI with method selection"),
|
|
42
42
|
"",
|
|
43
43
|
pastel.public_send(Tng::UI::Theme.color(:primary), "Features:"),
|
|
44
44
|
pastel.public_send(Tng::UI::Theme.color(:success),
|
|
45
|
-
"#{Tng::UI::Theme.icon(:bullet)} Test 20+ file types: Controllers, Models, Services + Jobs, Helpers, Lib, Policies, Presenters, Mailers, GraphQL, and more"),
|
|
45
|
+
"#{Tng::UI::Theme.icon(:bullet, ascii: true)} Test 20+ file types: Controllers, Models, Services + Jobs, Helpers, Lib, Policies, Presenters, Mailers, GraphQL, and more"),
|
|
46
46
|
pastel.public_send(Tng::UI::Theme.color(:success),
|
|
47
|
-
"#{Tng::UI::Theme.icon(:bullet)} Select specific methods to test"),
|
|
47
|
+
"#{Tng::UI::Theme.icon(:bullet, ascii: true)} Select specific methods to test"),
|
|
48
48
|
pastel.public_send(Tng::UI::Theme.color(:success),
|
|
49
|
-
"#{Tng::UI::Theme.icon(:bullet)} Search and filter through methods"),
|
|
49
|
+
"#{Tng::UI::Theme.icon(:bullet, ascii: true)} Search and filter through methods"),
|
|
50
50
|
"",
|
|
51
51
|
pastel.public_send(Tng::UI::Theme.color(:primary), "Help:"),
|
|
52
52
|
pastel.public_send(Tng::UI::Theme.color(:success),
|
|
53
|
-
"#{Tng::UI::Theme.icon(:bullet)} bundle exec tng --help") + pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
54
|
-
|
|
53
|
+
"#{Tng::UI::Theme.icon(:bullet, ascii: true)} bundle exec tng --help") + pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
54
|
+
" - Show help information"),
|
|
55
55
|
"",
|
|
56
56
|
pastel.public_send(Tng::UI::Theme.color(:muted),
|
|
57
|
-
"#{Tng::UI::Theme.icon(:lightbulb)} Generate tests for individual methods with precision")
|
|
57
|
+
"#{Tng::UI::Theme.icon(:lightbulb, ascii: true)} Generate tests for individual methods with precision")
|
|
58
58
|
].join("\n")
|
|
59
59
|
end
|
|
60
60
|
|
data/lib/tng/ui/theme.rb
CHANGED
|
@@ -26,6 +26,23 @@ module Tng
|
|
|
26
26
|
bullet: "•"
|
|
27
27
|
}.freeze
|
|
28
28
|
|
|
29
|
+
ICONS_ASCII = {
|
|
30
|
+
success: "[OK]",
|
|
31
|
+
error: "[ERR]",
|
|
32
|
+
warning: "[!]",
|
|
33
|
+
info: "[i]",
|
|
34
|
+
rocket: ">>",
|
|
35
|
+
run: ">",
|
|
36
|
+
wave: "hi",
|
|
37
|
+
stats: "[stats]",
|
|
38
|
+
config: "[cfg]",
|
|
39
|
+
heart: "<3",
|
|
40
|
+
lightbulb: "[tip]",
|
|
41
|
+
back: "<- ",
|
|
42
|
+
marker: ">",
|
|
43
|
+
bullet: "-"
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
29
46
|
# Colors - terminal-agnostic color scheme
|
|
30
47
|
COLORS = {
|
|
31
48
|
# Primary status colors
|
|
@@ -119,6 +136,11 @@ module Tng
|
|
|
119
136
|
@background_cache
|
|
120
137
|
end
|
|
121
138
|
|
|
139
|
+
def icon(key, ascii: false)
|
|
140
|
+
icons = ascii ? ICONS_ASCII : ICONS
|
|
141
|
+
icons[key] || ""
|
|
142
|
+
end
|
|
143
|
+
|
|
122
144
|
def get_background_color
|
|
123
145
|
return :dark unless $stdout.tty? && $stdin.tty? && interactive_session?
|
|
124
146
|
|
data/lib/tng/utils.rb
CHANGED
|
@@ -76,6 +76,69 @@ module Tng
|
|
|
76
76
|
false
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
def self.project_root
|
|
80
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
81
|
+
Rails.root.to_s
|
|
82
|
+
else
|
|
83
|
+
Dir.pwd
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.normalize_path(entry, root)
|
|
88
|
+
return nil unless entry.is_a?(String)
|
|
89
|
+
|
|
90
|
+
trimmed = entry.strip
|
|
91
|
+
return nil if trimmed.empty?
|
|
92
|
+
|
|
93
|
+
normalized = trimmed.tr("\\", "/")
|
|
94
|
+
if normalized.start_with?("/")
|
|
95
|
+
File.expand_path(normalized)
|
|
96
|
+
else
|
|
97
|
+
File.expand_path(normalized, root)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.ignored_path?(path, root: nil)
|
|
102
|
+
return false unless path
|
|
103
|
+
|
|
104
|
+
root ||= project_root
|
|
105
|
+
target = normalize_path(path.to_s, root)
|
|
106
|
+
return false unless target
|
|
107
|
+
|
|
108
|
+
ignore_files = Array(Tng.config[:ignore_files])
|
|
109
|
+
ignore_folders = Array(Tng.config[:ignore_folders])
|
|
110
|
+
|
|
111
|
+
ignore_files.each do |entry|
|
|
112
|
+
resolved = normalize_path(entry.to_s, root)
|
|
113
|
+
next unless resolved
|
|
114
|
+
return true if resolved == target
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
ignore_folders.each do |entry|
|
|
118
|
+
resolved = normalize_path(entry.to_s, root)
|
|
119
|
+
next unless resolved
|
|
120
|
+
resolved = resolved.end_with?("/") ? resolved.chomp("/") : resolved
|
|
121
|
+
return true if target == resolved || target.start_with?(resolved + "/")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.filter_ignored_files(files, root: nil, path_key: :path)
|
|
128
|
+
return [] if files.nil?
|
|
129
|
+
root ||= project_root
|
|
130
|
+
|
|
131
|
+
files.reject do |file|
|
|
132
|
+
candidate =
|
|
133
|
+
if file.is_a?(Hash)
|
|
134
|
+
file[path_key] || file[path_key.to_s]
|
|
135
|
+
else
|
|
136
|
+
file
|
|
137
|
+
end
|
|
138
|
+
ignored_path?(candidate, root: root)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
79
142
|
def self.save_test_file(test_content)
|
|
80
143
|
puts "📋 Raw API response: #{test_content[0..200]}..." if ENV["DEBUG"]
|
|
81
144
|
parsed_response = JSON.parse(test_content)
|
data/lib/tng/version.rb
CHANGED
data/lib/tng.rb
CHANGED
|
@@ -98,7 +98,9 @@ module Tng
|
|
|
98
98
|
base_url: "https://app.tng.sh/",
|
|
99
99
|
test_helper_path: nil,
|
|
100
100
|
authentication_enabled: false,
|
|
101
|
-
authentication_methods: []
|
|
101
|
+
authentication_methods: [],
|
|
102
|
+
ignore_files: [],
|
|
103
|
+
ignore_folders: []
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
def self.configure
|
|
@@ -148,4 +150,20 @@ module Tng
|
|
|
148
150
|
def self.authentication_enabled
|
|
149
151
|
@config[:authentication_enabled]
|
|
150
152
|
end
|
|
153
|
+
|
|
154
|
+
def self.ignore_files=(value)
|
|
155
|
+
@config[:ignore_files] = value
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.ignore_files
|
|
159
|
+
@config[:ignore_files]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.ignore_folders=(value)
|
|
163
|
+
@config[:ignore_folders] = value
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.ignore_folders
|
|
167
|
+
@config[:ignore_folders]
|
|
168
|
+
end
|
|
151
169
|
end
|