tng 0.2.3 → 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.
@@ -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 = 360 # 6 minutes total
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
- start_time = Time.now
25
-
26
- response = Tng.send_request_for_controller(
27
- controller[:name],
28
- controller[:path],
29
- method_info[:name],
30
- Tng::Utils.fixture_content,
31
- Tng::Services::UserAppConfig.config_with_source,
32
- Tng::Services::UserAppConfig.base_url,
33
- Tng::Services::UserAppConfig.api_key
34
- )
35
-
36
- job_data = JSON.parse(response.body)
37
- job_id = job_data["job_id"]
38
-
39
- return unless job_id
40
-
41
- result = poll_for_completion(job_id)
42
-
43
- return unless result
44
-
45
- end_time = Time.now
46
- generation_time = end_time - start_time
47
-
48
- file_result = Tng::Utils.save_test_file(result.to_json)
49
- return unless file_result
50
-
51
- file_result.merge(generation_time: generation_time)
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
- start_time = Time.now
56
-
57
- response = Tng.send_request_for_model(
58
- model[:name],
59
- model[:path],
60
- method_info[:name],
61
- Tng::Utils.fixture_content,
62
- Tng::Services::UserAppConfig.config_with_source,
63
- Tng::Services::UserAppConfig.base_url,
64
- Tng::Services::UserAppConfig.api_key
65
- )
66
-
67
- job_data = JSON.parse(response.body)
68
- job_id = job_data["job_id"]
69
-
70
- return unless job_id
71
-
72
- result = poll_for_completion(job_id)
73
-
74
- return unless result
75
-
76
- end_time = Time.now
77
- generation_time = end_time - start_time
28
+ generate_test_for_type(model, method_info, :model)
29
+ end
78
30
 
79
- file_result = Tng::Utils.save_test_file(result.to_json)
80
- return unless file_result
31
+ def run_for_service_method(service, method_info)
32
+ generate_test_for_type(service, method_info, :service)
33
+ end
81
34
 
82
- file_result.merge(generation_time: generation_time)
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
- def run_for_service_method(service, method_info)
39
+ private
40
+
41
+ def generate_test_for_type(file_object, method_info, type)
86
42
  start_time = Time.now
87
43
 
88
- response = Tng.send_request_for_service(
89
- service[:name],
90
- service[:path],
91
- method_info[:name],
92
- Tng::Utils.fixture_content,
93
- Tng::Services::UserAppConfig.config_with_source,
94
- Tng::Services::UserAppConfig.base_url,
95
- Tng::Services::UserAppConfig.api_key
96
- )
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 run_for_other_method(other_file, method_info)
117
- start_time = Time.now
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
- response = Tng.send_request_for_other(
120
- other_file[:name] || File.basename(other_file[:path], ".rb"),
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 = case seconds_elapsed
224
- when 0..15 then "initializing..."
225
- when 16..45 then "analyzing code structure..."
226
- when 46..90 then "generating test cases..."
227
- when 91..150 then "optimizing test logic..."
228
- when 151..210 then "refining assertions..."
229
- when 211..270 then "formatting output..."
230
- when 271..330 then "finalizing tests..."
231
- else "completing generation..."
232
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tng
4
- VERSION = "0.2.3"
4
+ VERSION = "0.2.4"
5
5
  end
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.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - ralucab
@@ -219,6 +219,7 @@ files:
219
219
  - lib/tng/railtie.rb
220
220
  - lib/tng/services/direct_generation.rb
221
221
  - lib/tng/services/extract_methods.rb
222
+ - lib/tng/services/file_type_detector.rb
222
223
  - lib/tng/services/test_generator.rb
223
224
  - lib/tng/services/testng.rb
224
225
  - lib/tng/services/user_app_config.rb
@@ -271,7 +272,7 @@ post_install_message: "┌ TNG ────────────────
271
272
  \ │\n│ • bundle exec
272
273
  tng --help - Show help information │\n│ │\n│
273
274
  \ \U0001F4A1 Generate tests for individual methods with precision │\n└────────────────────────────────────────────────────────────
274
- v0.2.3 ┘\n"
275
+ v0.2.4 ┘\n"
275
276
  rdoc_options: []
276
277
  require_paths:
277
278
  - lib