tng 0.2.2 → 0.2.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 +29 -2
- data/Rakefile +54 -9
- data/bin/tng +155 -51
- data/binaries/tng.bundle +0 -0
- data/lib/tng/analyzers/controller.rb +0 -31
- data/lib/tng/analyzers/model.rb +0 -31
- data/lib/tng/analyzers/other.rb +0 -27
- data/lib/tng/analyzers/service.rb +0 -31
- data/lib/tng/services/direct_generation.rb +52 -250
- data/lib/tng/services/extract_methods.rb +4 -8
- data/lib/tng/services/file_type_detector.rb +124 -0
- data/lib/tng/services/test_generator.rb +83 -163
- data/lib/tng/ui/authentication_warning_display.rb +0 -50
- data/lib/tng/ui/theme.rb +2 -1
- data/lib/tng/utils.rb +139 -0
- data/lib/tng/version.rb +1 -1
- data/tng.gemspec +1 -1
- metadata +4 -4
- data/binaries/tng.so +0 -0
@@ -12,7 +12,7 @@ module Tng
|
|
12
12
|
GENERATE_TESTS_PATH = "cli/tng_rails/contents/generate_tests"
|
13
13
|
CONTENT_RESPONSES_PATH = "cli/tng_rails/content_responses"
|
14
14
|
POLL_INTERVAL_SECONDS = 5 # Poll every 5 seconds
|
15
|
-
MAX_POLL_DURATION_SECONDS =
|
15
|
+
MAX_POLL_DURATION_SECONDS = 420 # 7 minutes total
|
16
16
|
|
17
17
|
def initialize(http_client)
|
18
18
|
@http_client = http_client
|
@@ -21,79 +21,36 @@ module Tng
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def run_for_controller_method(controller, method_info)
|
24
|
-
|
25
|
-
|
26
|
-
response = Tng.send_request_for_controller(
|
27
|
-
controller[:name],
|
28
|
-
controller[:path],
|
29
|
-
method_info[:name],
|
30
|
-
Tng::Utils.fixture_content,
|
31
|
-
Tng::Services::UserAppConfig.config_with_source,
|
32
|
-
Tng::Services::UserAppConfig.base_url,
|
33
|
-
Tng::Services::UserAppConfig.api_key
|
34
|
-
)
|
35
|
-
|
36
|
-
job_data = JSON.parse(response.body)
|
37
|
-
job_id = job_data["job_id"]
|
38
|
-
|
39
|
-
return unless job_id
|
40
|
-
|
41
|
-
result = poll_for_completion(job_id)
|
42
|
-
|
43
|
-
return unless result
|
44
|
-
|
45
|
-
end_time = Time.now
|
46
|
-
generation_time = end_time - start_time
|
47
|
-
|
48
|
-
file_result = Tng::Utils.save_test_file(result.to_json)
|
49
|
-
return unless file_result
|
50
|
-
|
51
|
-
file_result.merge(generation_time: generation_time)
|
24
|
+
generate_test_for_type(controller, method_info, :controller)
|
52
25
|
end
|
53
26
|
|
54
27
|
def run_for_model_method(model, method_info)
|
55
|
-
|
56
|
-
|
57
|
-
response = Tng.send_request_for_model(
|
58
|
-
model[:name],
|
59
|
-
model[:path],
|
60
|
-
method_info[:name],
|
61
|
-
Tng::Utils.fixture_content,
|
62
|
-
Tng::Services::UserAppConfig.config_with_source,
|
63
|
-
Tng::Services::UserAppConfig.base_url,
|
64
|
-
Tng::Services::UserAppConfig.api_key
|
65
|
-
)
|
66
|
-
|
67
|
-
job_data = JSON.parse(response.body)
|
68
|
-
job_id = job_data["job_id"]
|
69
|
-
|
70
|
-
return unless job_id
|
71
|
-
|
72
|
-
result = poll_for_completion(job_id)
|
73
|
-
|
74
|
-
return unless result
|
75
|
-
|
76
|
-
end_time = Time.now
|
77
|
-
generation_time = end_time - start_time
|
28
|
+
generate_test_for_type(model, method_info, :model)
|
29
|
+
end
|
78
30
|
|
79
|
-
|
80
|
-
|
31
|
+
def run_for_service_method(service, method_info)
|
32
|
+
generate_test_for_type(service, method_info, :service)
|
33
|
+
end
|
81
34
|
|
82
|
-
|
35
|
+
def run_for_other_method(other_file, method_info)
|
36
|
+
generate_test_for_type(other_file, method_info, :other)
|
83
37
|
end
|
84
38
|
|
85
|
-
|
39
|
+
private
|
40
|
+
|
41
|
+
def generate_test_for_type(file_object, method_info, type)
|
86
42
|
start_time = Time.now
|
87
43
|
|
88
|
-
response =
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
Tng::
|
93
|
-
Tng::
|
94
|
-
|
95
|
-
|
96
|
-
|
44
|
+
response = send_request_for_type(file_object, method_info, type)
|
45
|
+
return unless response
|
46
|
+
|
47
|
+
if response.is_a?(HTTPX::ErrorResponse)
|
48
|
+
error_icon = Tng::UI::Theme.icon(:error)
|
49
|
+
error_color = Tng::UI::Theme.color(:error)
|
50
|
+
puts "#{error_icon} #{@pastel.decorate("Request failed:",
|
51
|
+
error_color)} #{response.error&.message || "Unknown error"}"
|
52
|
+
return
|
53
|
+
end
|
97
54
|
|
98
55
|
job_data = JSON.parse(response.body)
|
99
56
|
job_id = job_data["job_id"]
|
@@ -113,103 +70,34 @@ module Tng
|
|
113
70
|
file_result.merge(generation_time: generation_time)
|
114
71
|
end
|
115
72
|
|
116
|
-
def
|
117
|
-
|
73
|
+
def send_request_for_type(file_object, method_info, type)
|
74
|
+
config = request_config
|
75
|
+
name = file_object[:name] || File.basename(file_object[:path], ".rb")
|
76
|
+
|
77
|
+
case type
|
78
|
+
when :controller
|
79
|
+
Tng.send_request_for_controller(name, file_object[:path], method_info[:name], *config)
|
80
|
+
when :model
|
81
|
+
Tng.send_request_for_model(name, file_object[:path], method_info[:name], *config)
|
82
|
+
when :service
|
83
|
+
Tng.send_request_for_service(name, file_object[:path], method_info[:name], *config)
|
84
|
+
when :other
|
85
|
+
Tng.send_request_for_other(name, file_object[:path], method_info[:name], *config)
|
86
|
+
end
|
87
|
+
end
|
118
88
|
|
119
|
-
|
120
|
-
|
121
|
-
other_file[:path],
|
122
|
-
method_info[:name],
|
89
|
+
def request_config
|
90
|
+
[
|
123
91
|
Tng::Utils.fixture_content,
|
124
92
|
Tng::Services::UserAppConfig.config_with_source,
|
125
93
|
Tng::Services::UserAppConfig.base_url,
|
126
94
|
Tng::Services::UserAppConfig.api_key
|
127
|
-
|
128
|
-
|
129
|
-
job_data = JSON.parse(response.body)
|
130
|
-
job_id = job_data["job_id"]
|
131
|
-
|
132
|
-
return unless job_id
|
133
|
-
|
134
|
-
result = poll_for_completion(job_id)
|
135
|
-
|
136
|
-
return unless result
|
137
|
-
|
138
|
-
end_time = Time.now
|
139
|
-
generation_time = end_time - start_time
|
140
|
-
|
141
|
-
file_result = Tng::Utils.save_test_file(result.to_json)
|
142
|
-
return unless file_result
|
143
|
-
|
144
|
-
file_result.merge(generation_time: generation_time)
|
145
|
-
end
|
146
|
-
|
147
|
-
private
|
148
|
-
|
149
|
-
def send_request_and_save_test(payload)
|
150
|
-
job_id = submit_async_job(payload)
|
151
|
-
return unless job_id
|
152
|
-
|
153
|
-
result = poll_for_completion(job_id)
|
154
|
-
return unless result
|
155
|
-
|
156
|
-
Tng::Utils.save_test_file(result.to_json)
|
157
|
-
rescue StandardError => e
|
158
|
-
debug_log("Async request failed: #{e.message}") if debug_enabled?
|
159
|
-
puts "❌ Failed to generate test: #{e.message}"
|
160
|
-
nil
|
161
|
-
end
|
162
|
-
|
163
|
-
def submit_async_job(payload)
|
164
|
-
marshaled = Marshal.dump(payload)
|
165
|
-
compressed = Zlib::Deflate.deflate(marshaled)
|
166
|
-
response = @http_client.post_binary(GENERATE_TESTS_PATH, compressed)
|
167
|
-
|
168
|
-
if response.is_a?(HTTPX::ErrorResponse)
|
169
|
-
debug_log("Processing test failed") if debug_enabled?
|
170
|
-
puts "❌ Failed to submit test generation job: #{response.error&.message}"
|
171
|
-
return
|
172
|
-
end
|
173
|
-
|
174
|
-
job_data = JSON.parse(response.body)
|
175
|
-
job_id = job_data["job_id"]
|
176
|
-
|
177
|
-
debug_log("Processing test with id: #{job_id}") if debug_enabled?
|
178
|
-
|
179
|
-
job_id
|
180
|
-
rescue JSON::ParserError => e
|
181
|
-
debug_log("Failed to parse API response: #{e.message}") if debug_enabled?
|
182
|
-
puts "❌ Failed to parse API response. Please retry."
|
95
|
+
]
|
183
96
|
end
|
184
97
|
|
185
98
|
def poll_for_completion(job_id)
|
186
99
|
start_time = Time.current
|
187
|
-
|
188
|
-
rocket_icon = Tng::UI::Theme.icon(:rocket)
|
189
|
-
success_color = Tng::UI::Theme.color(:success)
|
190
|
-
primary_color = Tng::UI::Theme.color(:primary)
|
191
|
-
muted_color = Tng::UI::Theme.color(:muted)
|
192
|
-
|
193
|
-
complete_char = @pastel.decorate("▓", success_color)
|
194
|
-
incomplete_char = @pastel.decorate("░", muted_color)
|
195
|
-
head_char = @pastel.decorate("▶", success_color)
|
196
|
-
|
197
|
-
progress_bar = TTY::ProgressBar.new(
|
198
|
-
"#{rocket_icon} #{@pastel.decorate("Generating tests",
|
199
|
-
primary_color)} #{@pastel.decorate("[:bar]",
|
200
|
-
:white)} #{@pastel.decorate(":status",
|
201
|
-
muted_color)} #{@pastel.decorate(
|
202
|
-
"(:elapsed)", muted_color
|
203
|
-
)}",
|
204
|
-
total: nil,
|
205
|
-
complete: complete_char,
|
206
|
-
incomplete: incomplete_char,
|
207
|
-
head: head_char,
|
208
|
-
width: 40,
|
209
|
-
clear: true,
|
210
|
-
frequency: 10, # Update 10 times per second for smooth animation
|
211
|
-
interval: 1 # Show elapsed time updates every second
|
212
|
-
)
|
100
|
+
progress_bar = create_progress_bar
|
213
101
|
|
214
102
|
loop do
|
215
103
|
seconds_elapsed = (Time.current - start_time).to_i
|
@@ -220,16 +108,7 @@ module Tng
|
|
220
108
|
return
|
221
109
|
end
|
222
110
|
|
223
|
-
status_text =
|
224
|
-
when 0..15 then "initializing..."
|
225
|
-
when 16..45 then "analyzing code structure..."
|
226
|
-
when 46..90 then "generating test cases..."
|
227
|
-
when 91..150 then "optimizing test logic..."
|
228
|
-
when 151..210 then "refining assertions..."
|
229
|
-
when 211..270 then "formatting output..."
|
230
|
-
when 271..330 then "finalizing tests..."
|
231
|
-
else "completing generation..."
|
232
|
-
end
|
111
|
+
status_text = determine_status_text(seconds_elapsed)
|
233
112
|
progress_bar.advance(1, status: status_text)
|
234
113
|
|
235
114
|
sleep(POLL_INTERVAL_SECONDS)
|
@@ -275,6 +154,47 @@ module Tng
|
|
275
154
|
end
|
276
155
|
end
|
277
156
|
|
157
|
+
def create_progress_bar
|
158
|
+
rocket_icon = Tng::UI::Theme.icon(:rocket)
|
159
|
+
success_color = Tng::UI::Theme.color(:success)
|
160
|
+
primary_color = Tng::UI::Theme.color(:primary)
|
161
|
+
muted_color = Tng::UI::Theme.color(:muted)
|
162
|
+
|
163
|
+
complete_char = @pastel.decorate("\u2593", success_color)
|
164
|
+
incomplete_char = @pastel.decorate("\u2591", muted_color)
|
165
|
+
head_char = @pastel.decorate("\u25B6", success_color)
|
166
|
+
|
167
|
+
TTY::ProgressBar.new(
|
168
|
+
"#{rocket_icon} #{@pastel.decorate("Generating tests",
|
169
|
+
primary_color)} #{@pastel.decorate("[:bar]",
|
170
|
+
:white)} #{@pastel.decorate(":status",
|
171
|
+
muted_color)} #{@pastel.decorate(
|
172
|
+
"(:elapsed)", muted_color
|
173
|
+
)}",
|
174
|
+
total: nil,
|
175
|
+
complete: complete_char,
|
176
|
+
incomplete: incomplete_char,
|
177
|
+
head: head_char,
|
178
|
+
width: 40,
|
179
|
+
clear: true,
|
180
|
+
frequency: 10, # Update 10 times per second for smooth animation
|
181
|
+
interval: 1 # Show elapsed time updates every second
|
182
|
+
)
|
183
|
+
end
|
184
|
+
|
185
|
+
def determine_status_text(seconds_elapsed)
|
186
|
+
case seconds_elapsed
|
187
|
+
when 0..15 then "initializing..."
|
188
|
+
when 16..45 then "analyzing code structure..."
|
189
|
+
when 46..90 then "generating test cases..."
|
190
|
+
when 91..150 then "optimizing test logic..."
|
191
|
+
when 151..210 then "refining assertions..."
|
192
|
+
when 211..270 then "formatting output..."
|
193
|
+
when 271..330 then "finalizing tests..."
|
194
|
+
else "completing generation..."
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
278
198
|
def debug_enabled?
|
279
199
|
ENV["DEBUG"] == "1"
|
280
200
|
end
|
@@ -119,54 +119,4 @@ class AuthenticationWarningDisplay
|
|
119
119
|
method.key?(:auth_type) && !method[:auth_type].to_s.strip.empty?
|
120
120
|
end
|
121
121
|
end
|
122
|
-
|
123
|
-
def build_missing_items_list
|
124
|
-
issues = []
|
125
|
-
|
126
|
-
if Tng.authentication_enabled && authentication_methods_empty?
|
127
|
-
issues << @pastel.public_send(Tng::UI::Theme.color(:error), " • authentication_methods array is empty")
|
128
|
-
end
|
129
|
-
|
130
|
-
if !authentication_methods_empty? && !authentication_methods_valid?
|
131
|
-
issues << @pastel.public_send(Tng::UI::Theme.color(:error), " • authentication_methods contains invalid entries")
|
132
|
-
|
133
|
-
Tng.authentication_methods.each_with_index do |method, index|
|
134
|
-
next if method.is_a?(Hash)
|
135
|
-
|
136
|
-
issues << @pastel.public_send(Tng::UI::Theme.color(:muted), " - Entry #{index + 1}: not a valid hash")
|
137
|
-
next
|
138
|
-
end
|
139
|
-
|
140
|
-
Tng.authentication_methods.each_with_index do |method, index|
|
141
|
-
next unless method.is_a?(Hash)
|
142
|
-
|
143
|
-
if !method.key?(:method) || method[:method].to_s.strip.empty?
|
144
|
-
issues << @pastel.public_send(Tng::UI::Theme.color(:muted),
|
145
|
-
" - Entry #{index + 1}: missing or empty 'method'")
|
146
|
-
end
|
147
|
-
|
148
|
-
if !method.key?(:file_location) || method[:file_location].to_s.strip.empty?
|
149
|
-
issues << @pastel.public_send(Tng::UI::Theme.color(:muted),
|
150
|
-
" - Entry #{index + 1}: missing or empty 'file_location'")
|
151
|
-
end
|
152
|
-
|
153
|
-
if !method.key?(:auth_type) || method[:auth_type].to_s.strip.empty?
|
154
|
-
issues << @pastel.public_send(Tng::UI::Theme.color(:muted),
|
155
|
-
" - Entry #{index + 1}: missing or empty 'auth_type'")
|
156
|
-
end
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
unless Tng.authentication_enabled
|
161
|
-
issues << @pastel.public_send(Tng::UI::Theme.color(:warning),
|
162
|
-
" • authentication_enabled is set to false")
|
163
|
-
end
|
164
|
-
|
165
|
-
if issues.empty?
|
166
|
-
@pastel.public_send(Tng::UI::Theme.color(:muted),
|
167
|
-
" • Configuration appears valid")
|
168
|
-
else
|
169
|
-
issues.join("\n")
|
170
|
-
end
|
171
|
-
end
|
172
122
|
end
|
data/lib/tng/ui/theme.rb
CHANGED
@@ -13,6 +13,7 @@ module Tng
|
|
13
13
|
|
14
14
|
# Action icons
|
15
15
|
rocket: "🚀",
|
16
|
+
run: "▶",
|
16
17
|
wave: "👋",
|
17
18
|
stats: "📊",
|
18
19
|
config: "📋",
|
@@ -174,7 +175,7 @@ module Tng
|
|
174
175
|
return false if ENV["BUNDLE_GEMFILE"]
|
175
176
|
return false if $PROGRAM_NAME&.include?("bundle")
|
176
177
|
return false if $PROGRAM_NAME&.include?("rails") && ARGV.any? { |arg| %w[server console runner].include?(arg) }
|
177
|
-
|
178
|
+
|
178
179
|
ENV["TNG_CLI"] == "true" || $PROGRAM_NAME&.include?("tng")
|
179
180
|
end
|
180
181
|
|
data/lib/tng/utils.rb
CHANGED
@@ -321,5 +321,144 @@ module Tng
|
|
321
321
|
false
|
322
322
|
end
|
323
323
|
end
|
324
|
+
|
325
|
+
# Test result parsing methods
|
326
|
+
def self.parse_rspec_json_results(output, exit_code, pastel, terminal_width)
|
327
|
+
# Parse RSpec JSON output
|
328
|
+
json_data = JSON.parse(output)
|
329
|
+
summary = json_data["summary"] || json_data
|
330
|
+
|
331
|
+
total = summary["example_count"] || summary["total_examples"] || 0
|
332
|
+
failures = summary["failure_count"] || summary["failures"] || 0
|
333
|
+
pending = summary["pending_count"] || summary["pending"] || 0
|
334
|
+
errors = summary["errors_outside_of_examples_count"] || 0
|
335
|
+
|
336
|
+
passed = total - failures - pending - errors
|
337
|
+
|
338
|
+
display_test_counts(passed, failures + errors, pending, total, exit_code.zero?, pastel, terminal_width)
|
339
|
+
rescue JSON::ParserError
|
340
|
+
# Fallback to text parsing if JSON fails
|
341
|
+
puts center_text_static(
|
342
|
+
pastel.decorate("JSON parsing failed, falling back to text parsing",
|
343
|
+
Tng::UI::Theme.color(:warning)), terminal_width
|
344
|
+
)
|
345
|
+
parse_rspec_results(output, exit_code, pastel, terminal_width)
|
346
|
+
end
|
347
|
+
|
348
|
+
def self.parse_rspec_results(output, exit_code, pastel, terminal_width)
|
349
|
+
# RSpec output example: "7 examples, 2 failures"
|
350
|
+
# or "Finished in 0.12345 seconds (files took 0.01234 seconds to load)"
|
351
|
+
# "7 examples, 2 failures, 1 pending"
|
352
|
+
|
353
|
+
lines = output.lines
|
354
|
+
summary_line = lines.find { |line| line.match?(/\d+ examples?,/) }
|
355
|
+
|
356
|
+
if summary_line
|
357
|
+
# Extract numbers from summary
|
358
|
+
match = summary_line.match(/(\d+) examples?, (\d+) failures?(?:, (\d+) pending)?/)
|
359
|
+
if match
|
360
|
+
total = match[1].to_i
|
361
|
+
failures = match[2].to_i
|
362
|
+
pending = match[3].to_i || 0
|
363
|
+
passed = total - failures - pending
|
364
|
+
|
365
|
+
display_test_counts(passed, failures, pending, total, exit_code == 0, pastel, terminal_width)
|
366
|
+
else
|
367
|
+
puts center_text_static(pastel.decorate("Could not parse RSpec results", Tng::UI::Theme.color(:error)),
|
368
|
+
terminal_width)
|
369
|
+
puts center_text_static(output.lines.last.strip, terminal_width) if output.lines.any?
|
370
|
+
end
|
371
|
+
else
|
372
|
+
puts center_text_static(pastel.decorate("No test results found", Tng::UI::Theme.color(:warning)),
|
373
|
+
terminal_width)
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
def self.parse_minitest_results(output, exit_code, pastel, terminal_width)
|
378
|
+
# Minitest output example: "7 tests, 15 assertions, 2 failures, 1 errors, 0 skips"
|
379
|
+
# or "Run options: --seed 12345"
|
380
|
+
# "7 tests, 2 assertions, 0 failures, 0 errors, 0 skips"
|
381
|
+
|
382
|
+
lines = output.lines
|
383
|
+
# Look for summary line - could be "X tests" or "X runs"
|
384
|
+
summary_line = lines.find { |line| line.match?(/\d+ (?:tests?|runs?),/) }
|
385
|
+
|
386
|
+
if summary_line
|
387
|
+
# Extract numbers from summary - handle both "tests" and "runs" format
|
388
|
+
match = summary_line.match(/(\d+) (?:tests?|runs?), (\d+) assertions?, (\d+) failures?, (\d+) errors?, (\d+) skips?/)
|
389
|
+
if match
|
390
|
+
total = match[1].to_i
|
391
|
+
failures = match[3].to_i
|
392
|
+
errors = match[4].to_i
|
393
|
+
skips = match[5].to_i
|
394
|
+
passed = total - failures - errors - skips
|
395
|
+
|
396
|
+
display_test_counts(passed, failures + errors, skips, total, exit_code.zero?, pastel, terminal_width)
|
397
|
+
else
|
398
|
+
puts center_text_static(pastel.decorate("Could not parse Minitest results", Tng::UI::Theme.color(:error)),
|
399
|
+
terminal_width)
|
400
|
+
puts center_text_static("Expected format: 'X tests, Y assertions, Z failures, A errors, B skips'",
|
401
|
+
terminal_width)
|
402
|
+
puts center_text_static("Got: #{summary_line.strip}", terminal_width)
|
403
|
+
end
|
404
|
+
else
|
405
|
+
puts center_text_static(pastel.decorate("No test results found", Tng::UI::Theme.color(:warning)),
|
406
|
+
terminal_width)
|
407
|
+
puts center_text_static("Looking for line with test counts...", terminal_width)
|
408
|
+
# Show last few lines for debugging
|
409
|
+
last_lines = output.lines.last(3).map(&:strip).join(" | ")
|
410
|
+
puts center_text_static("Last lines: #{last_lines}", terminal_width) if output.lines.any?
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
def self.display_test_counts(passed, failed, skipped, total, success, pastel, terminal_width)
|
415
|
+
passed_icon = pastel.decorate(Tng::UI::Theme.icon(:success), Tng::UI::Theme.color(:primary))
|
416
|
+
failed_icon = pastel.decorate(Tng::UI::Theme.icon(:error), Tng::UI::Theme.color(:primary))
|
417
|
+
skipped_icon = pastel.decorate("⏭️", Tng::UI::Theme.color(:accent))
|
418
|
+
total_icon = pastel.decorate(Tng::UI::Theme.icon(:marker), Tng::UI::Theme.color(:primary))
|
419
|
+
|
420
|
+
passed_text = pastel.decorate("#{passed_icon} #{passed} passed", Tng::UI::Theme.color(:success))
|
421
|
+
failed_text = pastel.decorate("#{failed_icon} #{failed} failed", Tng::UI::Theme.color(:error))
|
422
|
+
skipped_text = if skipped.positive?
|
423
|
+
pastel.decorate("#{skipped_icon} #{skipped} skipped",
|
424
|
+
Tng::UI::Theme.color(:warning))
|
425
|
+
else
|
426
|
+
nil
|
427
|
+
end
|
428
|
+
total_text = pastel.decorate("#{total_icon} #{total} total", Tng::UI::Theme.color(:secondary))
|
429
|
+
|
430
|
+
results = [passed_text, failed_text, skipped_text, total_text].compact.join(", ")
|
431
|
+
puts center_text_static(results, terminal_width)
|
432
|
+
|
433
|
+
# Overall result
|
434
|
+
overall_msg = if success
|
435
|
+
pastel.decorate("#{Tng::UI::Theme.icon(:success)} All tests passed!", Tng::UI::Theme.color(:success))
|
436
|
+
else
|
437
|
+
pastel.decorate("#{Tng::UI::Theme.icon(:error)} Some tests failed", Tng::UI::Theme.color(:error))
|
438
|
+
end
|
439
|
+
puts center_text_static(overall_msg, terminal_width)
|
440
|
+
end
|
441
|
+
|
442
|
+
def self.center_text_static(text, width = 80)
|
443
|
+
lines = text.split("\n")
|
444
|
+
lines.map do |line|
|
445
|
+
# Remove ANSI color codes for length calculation
|
446
|
+
clean_line = line.gsub(/\e\[[0-9;]*m/, "")
|
447
|
+
padding = [(width - clean_line.length) / 2, 0].max
|
448
|
+
" " * padding + line
|
449
|
+
end.join("\n")
|
450
|
+
end
|
451
|
+
|
452
|
+
def self.format_generation_time(seconds)
|
453
|
+
if seconds < 1
|
454
|
+
"#{(seconds * 1000).round}ms"
|
455
|
+
elsif seconds < 60
|
456
|
+
"#{seconds.round(1)}s"
|
457
|
+
else
|
458
|
+
minutes = (seconds / 60).floor
|
459
|
+
remaining_seconds = (seconds % 60).round
|
460
|
+
"#{minutes}m #{remaining_seconds}s"
|
461
|
+
end
|
462
|
+
end
|
324
463
|
end
|
325
464
|
end
|
data/lib/tng/version.rb
CHANGED
data/tng.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = "tng"
|
7
7
|
spec.version = Tng::VERSION
|
8
8
|
spec.authors = ["ralucab"]
|
9
|
-
spec.email = ["
|
9
|
+
spec.email = ["raluca@tng.sh"]
|
10
10
|
|
11
11
|
spec.summary = "TNG generates tests using static code analysis and LLM"
|
12
12
|
spec.description = "TNG (Test Next Generation) is a Rails gem that automatically generates comprehensive test files by analyzing your Ruby code using static analysis and AI. It supports models, controllers, and services with intelligent test case generation."
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tng
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ralucab
|
@@ -195,7 +195,7 @@ description: TNG (Test Next Generation) is a Rails gem that automatically genera
|
|
195
195
|
comprehensive test files by analyzing your Ruby code using static analysis and AI.
|
196
196
|
It supports models, controllers, and services with intelligent test case generation.
|
197
197
|
email:
|
198
|
-
-
|
198
|
+
- raluca@tng.sh
|
199
199
|
executables:
|
200
200
|
- load_dev
|
201
201
|
- tng
|
@@ -209,7 +209,6 @@ files:
|
|
209
209
|
- bin/load_dev
|
210
210
|
- bin/tng
|
211
211
|
- binaries/tng.bundle
|
212
|
-
- binaries/tng.so
|
213
212
|
- lib/generators/tng/install_generator.rb
|
214
213
|
- lib/tng.rb
|
215
214
|
- lib/tng/analyzers/controller.rb
|
@@ -220,6 +219,7 @@ files:
|
|
220
219
|
- lib/tng/railtie.rb
|
221
220
|
- lib/tng/services/direct_generation.rb
|
222
221
|
- lib/tng/services/extract_methods.rb
|
222
|
+
- lib/tng/services/file_type_detector.rb
|
223
223
|
- lib/tng/services/test_generator.rb
|
224
224
|
- lib/tng/services/testng.rb
|
225
225
|
- lib/tng/services/user_app_config.rb
|
@@ -272,7 +272,7 @@ post_install_message: "┌ TNG ────────────────
|
|
272
272
|
\ │\n│ • bundle exec
|
273
273
|
tng --help - Show help information │\n│ │\n│
|
274
274
|
\ \U0001F4A1 Generate tests for individual methods with precision │\n└────────────────────────────────────────────────────────────
|
275
|
-
v0.2.
|
275
|
+
v0.2.4 ┘\n"
|
276
276
|
rdoc_options: []
|
277
277
|
require_paths:
|
278
278
|
- lib
|
data/binaries/tng.so
DELETED
Binary file
|