Class: Tng::Services::TestGenerator
- Inherits:
-
Object
- Object
- Tng::Services::TestGenerator
- Defined in:
- lib/tng/services/test_generator.rb
Constant Summary collapse
- GENERATE_TESTS_PATH =
"cli/tng_rails/contents/generate_tests"- CONTENT_RESPONSES_PATH =
"cli/tng_rails/content_responses"- RUBY_CONTENT_RESPONSES_PATH =
"cli/tng_ruby/content_responses"- POLL_INTERVAL_SECONDS =
Poll every 5 seconds
5- MAX_POLL_DURATION_SECONDS =
7 minutes total
420
Instance Method Summary collapse
- #debug_enabled? ⇒ Boolean
- #debug_log(message) ⇒ Object
- #enrich_method_info_with_trace_v2(file_object, method_info, ruby_mode:) ⇒ Object
- #generate_test_for_ruby(file_object, method_info, progress: nil) ⇒ Object
- #generate_test_for_type(file_object, method_info, type, progress: nil) ⇒ Object
- #init_agent_steps(progress) ⇒ Object
-
#initialize(http_client) ⇒ TestGenerator
constructor
A new instance of TestGenerator.
-
#poll_for_completion(job_id, progress: nil, content_path: CONTENT_RESPONSES_PATH) ⇒ Object
Modified method signature and added interrupt handler.
- #request_config ⇒ Object
-
#run_audit_for_controller_method(controller, method_info, progress: nil) ⇒ Object
Audit mode - async, polls for completion.
- #run_audit_for_model_method(model, method_info, progress: nil) ⇒ Object
- #run_audit_for_other_method(other_file, method_info, progress: nil) ⇒ Object
- #run_audit_for_ruby(file_object, method_info, progress: nil) ⇒ Object
- #run_audit_for_ruby_method(other_file, method_info, progress: nil) ⇒ Object
- #run_audit_for_service_method(service, method_info, progress: nil) ⇒ Object
- #run_audit_for_type(file_object, method_info, type, progress: nil) ⇒ Object
- #run_for_controller_method(controller, method_info, progress: nil) ⇒ Object
- #run_for_model_method(model, method_info, progress: nil) ⇒ Object
- #run_for_other_method(other_file, method_info, progress: nil) ⇒ Object
- #run_for_ruby_method(other_file, method_info, progress: nil) ⇒ Object
- #run_for_service_method(service, method_info, progress: nil) ⇒ Object
- #send_audit_request_for_type(file_object, method_info, type) ⇒ Object
- #send_request_for_ruby(file_object, method_info, audit_mode:) ⇒ Object
- #send_request_for_type(file_object, method_info, type) ⇒ Object
- #trigger_cleanup(job_id, content_path: CONTENT_RESPONSES_PATH) ⇒ Object
- #update_progress_from_info(progress, agent_step_indices, info, percent) ⇒ Object
Constructor Details
#initialize(http_client) ⇒ TestGenerator
Returns a new instance of TestGenerator.
18 19 20 21 22 |
# File 'lib/tng/services/test_generator.rb', line 18 def initialize(http_client) @http_client = http_client @machine_info = Tng.machine_info @pastel = Pastel.new end |
Instance Method Details
#debug_enabled? ⇒ Boolean
414 415 416 |
# File 'lib/tng/services/test_generator.rb', line 414 def debug_enabled? ENV["DEBUG"] == "1" end |
#debug_log(message) ⇒ Object
418 419 420 |
# File 'lib/tng/services/test_generator.rb', line 418 def debug_log() puts "-> DEBUG [TestGenerator]: #{message}" end |
#enrich_method_info_with_trace_v2(file_object, method_info, ruby_mode:) ⇒ Object
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/tng/services/test_generator.rb', line 232 def enrich_method_info_with_trace_v2(file_object, method_info, ruby_mode:) return method_info unless method_info.is_a?(Hash) return method_info if method_info.key?(:trace_v2) || method_info.key?("trace_v2") method_name = method_info[:name] || method_info["name"] return method_info if method_name.to_s.empty? class_name = method_info[:class_name] || method_info["class_name"] project_root = Dir.pwd path = File.(file_object[:path]) trace_v2 = if ruby_mode Tng::Analyzer::Context.analyze_symbolic_trace_ruby_v2(project_root, path, method_name, class_name) else Tng::Analyzer::Context.analyze_symbolic_trace_v2(project_root, path, method_name, class_name) end method_info.merge(trace_v2: trace_v2) rescue StandardError => e debug_log("Trace v2 enrichment failed for #{file_object[:path]}##{method_name}: #{e.message}") if debug_enabled? method_info end |
#generate_test_for_ruby(file_object, method_info, progress: nil) ⇒ Object
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/tng/services/test_generator.rb', line 167 def generate_test_for_ruby(file_object, method_info, progress: nil) start_time = Time.now response = send_request_for_ruby(file_object, method_info, audit_mode: false) return unless response if response.is_a?(HTTPX::ErrorResponse) return { error: :network_error, message: response.error&. || "Network error" } end job_data = JSON.parse(response.body) return { error: :auth_failed, message: "Invalid or missing API key" } if response.status == 401 return { error: :auth_failed, message: "API key expired or usage limit reached" } if response.status == 403 job_id = job_data["job_id"] return unless job_id result = poll_for_completion(job_id, progress: progress, content_path: RUBY_CONTENT_RESPONSES_PATH) return unless result end_time = Time.now generation_time = end_time - start_time file_result = Tng::Utils.save_test_file(result.to_json) return unless file_result trigger_cleanup(job_id, content_path: RUBY_CONTENT_RESPONSES_PATH) file_result.merge(generation_time: generation_time) end |
#generate_test_for_type(file_object, method_info, type, progress: nil) ⇒ Object
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/tng/services/test_generator.rb', line 112 def generate_test_for_type(file_object, method_info, type, progress: nil) start_time = Time.now response = send_request_for_type(file_object, method_info, type) return unless response if response.is_a?(HTTPX::ErrorResponse) return { error: :network_error, message: response.error&. || "Network error" } end job_data = JSON.parse(response.body) # Check for authentication errors return { error: :auth_failed, message: "Invalid or missing API key" } if response.status == 401 # Check for forbidden responses (usage limits or invalid keys) return { error: :auth_failed, message: "API key expired or usage limit reached" } if response.status == 403 job_id = job_data["job_id"] return unless job_id result = poll_for_completion(job_id, progress: progress, content_path: CONTENT_RESPONSES_PATH) return unless result end_time = Time.now generation_time = end_time - start_time file_result = Tng::Utils.save_test_file(result.to_json) return unless file_result trigger_cleanup(job_id, content_path: CONTENT_RESPONSES_PATH) file_result.merge(generation_time: generation_time) end |
#init_agent_steps(progress) ⇒ Object
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 |
# File 'lib/tng/services/test_generator.rb', line 344 def init_agent_steps(progress) return {} unless progress base_idx = 4 progress.update("Context Builder: Pending...", nil, step_increment: true) progress.update("Style Analyzer: Pending...", nil, step_increment: true) progress.update("Logic Analyzer: Pending...", nil, step_increment: true) progress.update("Logic Generator: Pending...", nil, step_increment: true) { "context_agent_status" => base_idx, "style_agent_status" => base_idx + 1, "logical_issue_status" => base_idx + 2, "behavior_expert_status" => base_idx + 3 } end |
#poll_for_completion(job_id, progress: nil, content_path: CONTENT_RESPONSES_PATH) ⇒ Object
Modified method signature and added interrupt handler
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 |
# File 'lib/tng/services/test_generator.rb', line 266 def poll_for_completion(job_id, progress: nil, content_path: CONTENT_RESPONSES_PATH) start_time = Time.now attempts = 0 max_attempts = MAX_POLL_DURATION_SECONDS / POLL_INTERVAL_SECONDS # Initialize agent steps if progress tracking is enabled agent_step_indices = init_agent_steps(progress) loop do attempts += 1 seconds_elapsed = (Time.now - start_time).to_i if seconds_elapsed > MAX_POLL_DURATION_SECONDS return { error: :timeout, message: "Test generation timed out after #{MAX_POLL_DURATION_SECONDS} seconds" } end # Calculate pseudo-percentage if progress pct = ((attempts.to_f / max_attempts.to_f) * 95.0).to_i # Update global progress bar without adding a new step (explicit_step=4 updates Context Builder line, but we want to just update percentage?) # The progress bar component takes the percent from the *last* update message. # So we can pass specific percent when we update any agent step. else pct = 0 end sleep(POLL_INTERVAL_SECONDS) status_response = @http_client.get("#{content_path}/#{job_id}") if status_response.is_a?(HTTPX::ErrorResponse) debug_log("Status check failed: #{status_response.error&.message}") if debug_enabled? next end begin status_data = JSON.parse(status_response.body.to_s, symbolize_names: true) status = status_data[:status] # Update UI with granular info update_progress_from_info(progress, agent_step_indices, status_data[:info], pct) if progress case status when "completed" debug_log("Test generation completed!") if debug_enabled? return status_data[:result].merge(analysis: status_data[:analysis]) when "failed" debug_log("Test generation failed: #{status_data[:error]}") if debug_enabled? return { error: :generation_failed, message: status_data[:error] } when "pending", "processing" next else debug_log("Unknown test generation status: #{status}") if debug_enabled? next end rescue JSON::ParserError => e debug_log("JSON parse error: #{e.message}") if debug_enabled? next end end rescue Interrupt # User pressed Ctrl+C - exit gracefully system("clear") || system("cls") puts "\n\n#{TTY::Box.frame( "Generation cancelled by user", padding: 1, align: :center, border: :thick, style: { fg: :yellow, border: { fg: :yellow } } )}" exit(0) end |
#request_config ⇒ Object
256 257 258 259 260 261 262 263 |
# File 'lib/tng/services/test_generator.rb', line 256 def request_config [ Tng::Utils.fixture_content, Tng::Services::UserAppConfig.config_with_source, Tng::Services::UserAppConfig.base_url, Tng::Services::UserAppConfig.api_key ] end |
#run_audit_for_controller_method(controller, method_info, progress: nil) ⇒ Object
Audit mode - async, polls for completion
46 47 48 |
# File 'lib/tng/services/test_generator.rb', line 46 def run_audit_for_controller_method(controller, method_info, progress: nil) run_audit_for_type(controller, method_info, :controller, progress: progress) end |
#run_audit_for_model_method(model, method_info, progress: nil) ⇒ Object
50 51 52 |
# File 'lib/tng/services/test_generator.rb', line 50 def run_audit_for_model_method(model, method_info, progress: nil) run_audit_for_type(model, method_info, :model, progress: progress) end |
#run_audit_for_other_method(other_file, method_info, progress: nil) ⇒ Object
58 59 60 |
# File 'lib/tng/services/test_generator.rb', line 58 def run_audit_for_other_method(other_file, method_info, progress: nil) run_audit_for_type(other_file, method_info, :other, progress: progress) end |
#run_audit_for_ruby(file_object, method_info, progress: nil) ⇒ Object
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/tng/services/test_generator.rb', line 198 def run_audit_for_ruby(file_object, method_info, progress: nil) response = send_request_for_ruby(file_object, method_info, audit_mode: true) return { error: :network_error, message: "Network error" } unless response if response.is_a?(HTTPX::ErrorResponse) return { error: :network_error, message: response.error&. || "Network error" } end return { error: :auth_failed, message: "Invalid or missing API key" } if response.status == 401 return { error: :auth_failed, message: "API key expired" } if response.status == 403 begin job_data = JSON.parse(response.body) return { error: :server_error, message: job_data["error"] } if job_data["error"] job_id = job_data["job_id"] return { error: :server_error, message: "No job_id returned" } unless job_id result = poll_for_completion(job_id, progress: progress, content_path: RUBY_CONTENT_RESPONSES_PATH) return { error: :timeout, message: "Audit timed out" } unless result { audit_results: result } rescue JSON::ParserError => e { error: :parse_error, message: "Failed to parse audit response: #{e.message}" } end end |
#run_audit_for_ruby_method(other_file, method_info, progress: nil) ⇒ Object
62 63 64 |
# File 'lib/tng/services/test_generator.rb', line 62 def run_audit_for_ruby_method(other_file, method_info, progress: nil) run_audit_for_ruby(other_file, method_info, progress: progress) end |
#run_audit_for_service_method(service, method_info, progress: nil) ⇒ Object
54 55 56 |
# File 'lib/tng/services/test_generator.rb', line 54 def run_audit_for_service_method(service, method_info, progress: nil) run_audit_for_type(service, method_info, :service, progress: progress) end |
#run_audit_for_type(file_object, method_info, type, progress: nil) ⇒ Object
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/tng/services/test_generator.rb', line 65 def run_audit_for_type(file_object, method_info, type, progress: nil) response = send_audit_request_for_type(file_object, method_info, type) return { error: :network_error, message: "Network error" } unless response if response.is_a?(HTTPX::ErrorResponse) return { error: :network_error, message: response.error&. || "Network error" } end return { error: :auth_failed, message: "Invalid or missing API key" } if response.status == 401 return { error: :auth_failed, message: "API key expired" } if response.status == 403 begin job_data = JSON.parse(response.body) return { error: :server_error, message: job_data["error"] } if job_data["error"] job_id = job_data["job_id"] return { error: :server_error, message: "No job_id returned" } unless job_id # Poll for completion (audit only) result = poll_for_completion(job_id, progress: progress, content_path: CONTENT_RESPONSES_PATH) return { error: :timeout, message: "Audit timed out" } unless result # Audit results come back as the result directly (not wrapped) # Wrap in audit_results key for save_audit_file compatibility { audit_results: result } rescue JSON::ParserError => e { error: :parse_error, message: "Failed to parse audit response: #{e.message}" } end end |
#run_for_controller_method(controller, method_info, progress: nil) ⇒ Object
24 25 26 |
# File 'lib/tng/services/test_generator.rb', line 24 def run_for_controller_method(controller, method_info, progress: nil) generate_test_for_type(controller, method_info, :controller, progress: progress) end |
#run_for_model_method(model, method_info, progress: nil) ⇒ Object
28 29 30 |
# File 'lib/tng/services/test_generator.rb', line 28 def run_for_model_method(model, method_info, progress: nil) generate_test_for_type(model, method_info, :model, progress: progress) end |
#run_for_other_method(other_file, method_info, progress: nil) ⇒ Object
36 37 38 |
# File 'lib/tng/services/test_generator.rb', line 36 def run_for_other_method(other_file, method_info, progress: nil) generate_test_for_type(other_file, method_info, :other, progress: progress) end |
#run_for_ruby_method(other_file, method_info, progress: nil) ⇒ Object
40 41 42 |
# File 'lib/tng/services/test_generator.rb', line 40 def run_for_ruby_method(other_file, method_info, progress: nil) generate_test_for_ruby(other_file, method_info, progress: progress) end |
#run_for_service_method(service, method_info, progress: nil) ⇒ Object
32 33 34 |
# File 'lib/tng/services/test_generator.rb', line 32 def run_for_service_method(service, method_info, progress: nil) generate_test_for_type(service, method_info, :service, progress: progress) end |
#send_audit_request_for_type(file_object, method_info, type) ⇒ Object
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/tng/services/test_generator.rb', line 95 def send_audit_request_for_type(file_object, method_info, type) config = request_config name = file_object[:name] || File.basename(file_object[:path], ".rb") enriched_method_info = enrich_method_info_with_trace_v2(file_object, method_info, ruby_mode: false) # Pass audit_mode: true as 8th parameter case type when :controller Tng.send_request_for_controller(name, file_object[:path], enriched_method_info, *config, true, false) when :model Tng.send_request_for_model(name, file_object[:path], enriched_method_info, *config, true, false) when :service Tng.send_request_for_service(name, file_object[:path], enriched_method_info, *config, true, false) when :other Tng.send_request_for_other(name, file_object[:path], enriched_method_info, *config, true, false) end end |
#send_request_for_ruby(file_object, method_info, audit_mode:) ⇒ Object
224 225 226 227 228 229 230 |
# File 'lib/tng/services/test_generator.rb', line 224 def send_request_for_ruby(file_object, method_info, audit_mode:) config = request_config name = file_object[:name] || File.basename(file_object[:path], ".rb") enriched_method_info = enrich_method_info_with_trace_v2(file_object, method_info, ruby_mode: true) Tng.send_request_for_ruby(name, file_object[:path], enriched_method_info, *config, audit_mode, false) end |
#send_request_for_type(file_object, method_info, type) ⇒ Object
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/tng/services/test_generator.rb', line 149 def send_request_for_type(file_object, method_info, type) config = request_config name = file_object[:name] || File.basename(file_object[:path], ".rb") enriched_method_info = enrich_method_info_with_trace_v2(file_object, method_info, ruby_mode: false) # Pass audit_mode: false as 8th parameter for normal test generation case type when :controller Tng.send_request_for_controller(name, file_object[:path], enriched_method_info, *config, false, false) when :model Tng.send_request_for_model(name, file_object[:path], enriched_method_info, *config, false, false) when :service Tng.send_request_for_service(name, file_object[:path], enriched_method_info, *config, false, false) when :other Tng.send_request_for_other(name, file_object[:path], enriched_method_info, *config, false, false) end end |
#trigger_cleanup(job_id, content_path: CONTENT_RESPONSES_PATH) ⇒ Object
408 409 410 411 412 |
# File 'lib/tng/services/test_generator.rb', line 408 def trigger_cleanup(job_id, content_path: CONTENT_RESPONSES_PATH) @http_client.patch("#{content_path}/#{job_id}/cleanup") rescue StandardError => e debug_log("Cleanup request failed: #{e.message}") if debug_enabled? end |
#update_progress_from_info(progress, agent_step_indices, info, percent) ⇒ Object
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 |
# File 'lib/tng/services/test_generator.rb', line 361 def update_progress_from_info(progress, agent_step_indices, info, percent) return unless progress && info.is_a?(Hash) agent_step_indices.each do |key, step_idx| item_data = info[key.to_s] || info[key.to_sym] next unless item_data agent_status = "pending" values = [] if item_data.is_a?(Hash) agent_status = item_data[:status] || item_data["status"] || "pending" values = item_data[:values] || item_data["values"] || [] else agent_status = item_data.to_s end label = case key when "context_agent_status" then "Context Builder" when "style_agent_status" then "Style Analyzer" when "logical_issue_status" then "Logic Analyzer" when "behavior_expert_status" then "Logic Generator" else key end msg = if agent_status == "processing" "#{label}: Processing..." elsif agent_status == "completed" "#{label}: Completed" elsif agent_status == "failed" "#{label}: Failed" else "#{label}: #{agent_status.capitalize}..." end if values.any? clean_vals = values.map { |v| v.to_s.gsub("_", " ").gsub("'", "").gsub(":", "").strip } display_str = clean_vals.first(3).join(", ") display_str += ", ..." if clean_vals.size > 3 msg += " (#{display_str})" end p = (percent && step_idx == agent_step_indices["behavior_expert_status"]) ? percent : nil progress.update(msg, p, step_increment: false, explicit_step: step_idx) end end |