tng 0.3.9 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8caa50f15bf264b0c8c1ec375469881d00c1137385f127c28c1e4a8a18bb9554
4
- data.tar.gz: 56c8dde536a3b54f1e8d0020aae0093b9014b749af79511df5903f920f66c6dd
3
+ metadata.gz: 4090a9996518027b0e5d49534360ce2269a735c407dcdb5544e4842a1431b590
4
+ data.tar.gz: 1171ff1e89fe751cb2a4de7a8cb688db9c65b22babdb7c1164f5d24032cc1092
5
5
  SHA512:
6
- metadata.gz: 185d1c9670f66fdd3eb2f469c5109bbdf04c8ca2157c9de3983cd332972dc30c8038926b95013a8a1a2bc6b21d12be9a3f8efb060787936a97202bc0d526d8a1
7
- data.tar.gz: 4374f5134a64c7b3c5c3d0b4a9b4bd785c555b0932a7552d285b0aa74ebadd1745a7b0395dcaef257426cbbc3e5328d2ca026fa9650d78147cbd4d5c48971801
6
+ metadata.gz: af6285fd48d5c93a1fad7698b1636aa03fef7f8f6f2b4705ed68390d1b0ee78a2aa118836952c7b2aac63dfaec417f23d9eaf99134e604ce43bed85576b3ddb6
7
+ data.tar.gz: 24f76ab0623a752f753b26c84b22ec8b971970de6e8493ea0f52c024eb94ee5fcd3f8459d4845782d01dcc23e92f8ac551136fec3a533be7b0ebac09812c973f
data/bin/tng CHANGED
@@ -64,6 +64,11 @@ class CLI
64
64
  desc "Run in audit mode (find issues and behaviours instead of generating tests)"
65
65
  end
66
66
 
67
+ flag :json do
68
+ long "--json"
69
+ desc "Output results in JSON format"
70
+ end
71
+
67
72
  flag :fix do
68
73
  short "-x"
69
74
  long "--fix"
@@ -78,7 +83,13 @@ class CLI
78
83
 
79
84
  def initialize
80
85
  @pastel = Pastel.new
81
- @go_ui = Tng::UI::GoUISession.new
86
+ # Check for --json flag raw before parsing
87
+ if ARGV.any? { |arg| arg == "--json" }
88
+ require "tng/ui/json_session"
89
+ @go_ui = Tng::UI::JsonSession.new
90
+ else
91
+ @go_ui = Tng::UI::GoUISession.new
92
+ end
82
93
  @terminal_width = begin
83
94
  TTY::Screen.width
84
95
  rescue StandardError
@@ -138,6 +149,8 @@ class CLI
138
149
  normalized << "--method=#{::Regexp.last_match(2)}"
139
150
  when /^(?:--)?(audit|a)$/
140
151
  normalized << "--audit"
152
+ when /^(?:--)?(json)$/
153
+ normalized << "--json"
141
154
  when /^(?:--)?(fix|x)$/
142
155
  normalized << "--fix"
143
156
  when /^(help|h)=(.+)$/
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
data/binaries/tng.bundle CHANGED
Binary file
@@ -64,12 +64,6 @@ module Tng
64
64
  name: method_name.to_s
65
65
  }
66
66
  end
67
- rescue NameError => e
68
- puts "❌ Could not load controller class #{controller_name}: #{e.message}"
69
- []
70
- rescue StandardError => e
71
- puts "❌ Error analyzing controller #{controller_name}: #{e.message}"
72
- []
73
67
  end
74
68
  end
75
69
  end
@@ -31,7 +31,8 @@ module Tng
31
31
  model_class = model_name.constantize
32
32
 
33
33
  instance_methods = model_class.public_instance_methods(false) +
34
- model_class.private_instance_methods(false)
34
+ model_class.protected_instance_methods(false) +
35
+ model_class.private_instance_methods(false)
35
36
  class_methods = model_class.public_methods(false) - Class.public_methods
36
37
 
37
38
  model_file = Object.const_source_location(model_class.name)&.first
@@ -78,12 +79,6 @@ module Tng
78
79
  else
79
80
  []
80
81
  end
81
- rescue NameError => e
82
- puts "❌ Could not load model class #{model_name}: #{e.message}"
83
- []
84
- rescue StandardError => e
85
- puts "❌ Error analyzing model #{model_name}: #{e.message}"
86
- []
87
82
  end
88
83
  end
89
84
 
@@ -109,7 +104,7 @@ module Tng
109
104
  node.arguments.arguments.each do |arg|
110
105
  if arg.is_a?(Prism::SymbolNode)
111
106
  validation_method = arg.value
112
- validations << validation_method if validation_method
107
+ validations << "validate :#{validation_method}" if validation_method
113
108
  end
114
109
  end
115
110
  else
@@ -117,7 +112,7 @@ module Tng
117
112
  node.arguments.arguments.each do |arg|
118
113
  if arg.is_a?(Prism::SymbolNode)
119
114
  attr_name = arg.value
120
- validations << attr_name if attr_name
115
+ validations << "validates :#{attr_name}" if attr_name
121
116
  end
122
117
  end
123
118
  end
@@ -32,7 +32,7 @@ module Tng
32
32
  service_class = service_name.constantize
33
33
 
34
34
  instance_methods = service_class.public_instance_methods(false) +
35
- service_class.private_instance_methods(false)
35
+ service_class.private_instance_methods(false)
36
36
  class_methods = service_class.public_methods(false) - Class.public_methods
37
37
 
38
38
  # Try to get source file from any method, fallback to const_source_location
@@ -74,12 +74,6 @@ module Tng
74
74
  end
75
75
 
76
76
  service_methods.map { |method_name| { name: method_name.to_s } }
77
- rescue NameError => e
78
- puts "❌ Could not load service class #{service_name}: #{e.message}"
79
- []
80
- rescue StandardError => e
81
- puts "❌ Error analyzing service #{service_name}: #{e.message}"
82
- []
83
77
  end
84
78
  end
85
79
 
@@ -33,17 +33,17 @@ module Tng
33
33
  method_name = @params[:method]
34
34
 
35
35
  unless file_path && method_name
36
- puts @pastel.red("❌ Both file and method parameters are required")
37
- puts @pastel.yellow("Usage: bundle exec tng app/controllers/users_controller.rb index")
38
- puts @pastel.yellow(" or: bundle exec tng --file=users_controller.rb --method=index")
39
- puts @pastel.yellow(" or: bundle exec tng f=users_controller.rb m=index")
36
+ @go_ui.display_error(@pastel.red("❌ Both file and method parameters are required"))
37
+ @go_ui.display_warning(@pastel.yellow("Usage: bundle exec tng app/controllers/users_controller.rb index"))
38
+ @go_ui.display_warning(@pastel.yellow(" or: bundle exec tng --file=users_controller.rb --method=index"))
39
+ @go_ui.display_warning(@pastel.yellow(" or: bundle exec tng f=users_controller.rb m=index"))
40
40
  return
41
41
  end
42
42
 
43
43
  resolved_path = FileTypeDetector.resolve_file_path(file_path)
44
44
 
45
45
  unless resolved_path && File.exist?(resolved_path)
46
- puts @pastel.red("❌ File not found: #{file_path}")
46
+ @go_ui.display_error(@pastel.red("❌ File not found: #{file_path}"))
47
47
  suggest_similar_files(file_path)
48
48
  return
49
49
  end
@@ -56,14 +56,13 @@ module Tng
56
56
 
57
57
  def suggest_similar_files(file_path)
58
58
  base_name = File.basename(file_path, ".rb")
59
- puts @pastel.yellow("💡 Did you mean one of these?")
60
59
 
61
60
  similar_files = find_similar_files(base_name)
62
61
 
63
62
  if similar_files.empty?
64
- puts @pastel.dim(" No similar files found")
63
+ @go_ui.display_info(@pastel.dim(" No similar files found"))
65
64
  else
66
- similar_files.first(5).each { |file| puts @pastel.dim(" #{file}") }
65
+ @go_ui.display_list(@pastel.yellow("💡 Did you mean one of these?"), similar_files.first(5))
67
66
  end
68
67
  end
69
68
 
@@ -87,25 +86,26 @@ module Tng
87
86
  methods = extract_methods_for_type(file_object, type)
88
87
 
89
88
  if methods.empty?
90
- puts @pastel.yellow("⚠️ No methods found in #{file_object[:name]}")
89
+ @go_ui.display_warning(@pastel.yellow("⚠️ No methods found in #{file_object[:name]}"))
91
90
  return
92
91
  end
93
92
 
94
93
  method_info = methods.find { |m| m[:name].downcase == method_name.downcase }
95
94
 
96
95
  unless method_info
97
- puts @pastel.red("❌ Method '#{method_name}' not found in #{file_object[:name]}")
98
- puts @pastel.yellow("Available methods:")
99
- methods.first(10).each { |m| puts @pastel.dim(" • #{m[:name]}") }
96
+ @go_ui.display_error(@pastel.red("❌ Method '#{method_name}' not found in #{file_object[:name]}"))
97
+ @go_ui.display_list(@pastel.yellow("Available methods:"), methods.first(10).map { |m| m[:name] })
100
98
  return
101
99
  end
102
100
 
103
- puts @pastel.bright_white("🎯 #{@params[:audit] ? "Auditing" : "Generating test for"} #{file_object[:name]}##{method_info[:name]}...")
101
+ @go_ui.display_info(@pastel.bright_white("🎯 #{@params[:audit] ? "Auditing" : "Generating test for"} #{file_object[:name]}##{method_info[:name]}..."))
104
102
 
105
- result = generate_test_result(file_object, method_info, type)
103
+ result = @go_ui.show_progress("Processing...") do |progress|
104
+ generate_test_result(file_object, method_info, type, progress)
105
+ end
106
106
 
107
107
  if result&.dig(:error)
108
- puts @pastel.red("❌ #{@params[:audit] ? "Audit" : "Test generation"} failed: #{result[:message]}")
108
+ @go_ui.display_error(@pastel.red("❌ #{@params[:audit] ? "Audit" : "Test generation"} failed: #{result[:message]}"))
109
109
  elsif @params[:audit]
110
110
  # Audit mode - display results inline
111
111
  display_audit_results(result)
@@ -113,7 +113,7 @@ module Tng
113
113
  # Test generation mode - show post-generation menu
114
114
  @show_post_generation_menu.call(result)
115
115
  else
116
- puts @pastel.red("❌ Failed to generate test")
116
+ @go_ui.display_error(@pastel.red("❌ Failed to generate test"))
117
117
  end
118
118
  end
119
119
 
@@ -126,22 +126,24 @@ module Tng
126
126
  end
127
127
  end
128
128
 
129
- def generate_test_result(file_object, method_info, type)
129
+ def generate_test_result(file_object, method_info, type, progress)
130
130
  generator = Tng::Services::TestGenerator.new(@http_client)
131
131
 
132
132
  if @params[:audit]
133
133
  # Audit mode - return issues and behaviours
134
134
  case type
135
- when "controller" then generator.run_audit_for_controller_method(file_object, method_info)
136
- else { error: :unsupported, message: "Audit mode only supports controllers currently" }
135
+ when "controller" then generator.run_audit_for_controller_method(file_object, method_info, progress: progress)
136
+ when "model" then generator.run_audit_for_model_method(file_object, method_info, progress: progress)
137
+ when "service" then generator.run_audit_for_service_method(file_object, method_info, progress: progress)
138
+ else generator.run_audit_for_other_method(file_object, method_info, progress: progress)
137
139
  end
138
140
  else
139
141
  # Test generation mode
140
142
  case type
141
- when "controller" then generator.run_for_controller_method(file_object, method_info)
142
- when "model" then generator.run_for_model_method(file_object, method_info)
143
- when "service" then generator.run_for_service_method(file_object, method_info)
144
- else generator.run_for_other_method(file_object, method_info)
143
+ when "controller" then generator.run_for_controller_method(file_object, method_info, progress: progress)
144
+ when "model" then generator.run_for_model_method(file_object, method_info, progress: progress)
145
+ when "service" then generator.run_for_service_method(file_object, method_info, progress: progress)
146
+ else generator.run_for_other_method(file_object, method_info, progress: progress)
145
147
  end
146
148
  end
147
149
  end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Tng
6
+ module UI
7
+ # UI Session that outputs JSON events to STDOUT for machine consumption
8
+ class JsonSession
9
+ def initialize
10
+ # Ensure stdout is sync so events are emitted immediately
11
+ $stdout.sync = true
12
+ @running = false
13
+ end
14
+
15
+ def start
16
+ @running = true
17
+ emit_event("started", { message: "TNG JSON Session Started" })
18
+ end
19
+
20
+ def stop
21
+ @running = false
22
+ emit_event("stopped", { message: "TNG JSON Session Stopped" })
23
+ end
24
+
25
+ def display_error(message)
26
+ emit_event("error", { message: strip_colors(message) })
27
+ end
28
+
29
+ def display_warning(message)
30
+ emit_event("warning", { message: strip_colors(message) })
31
+ end
32
+
33
+ def display_info(message)
34
+ emit_event("info", { message: strip_colors(message) })
35
+ end
36
+
37
+ def display_list(title, items)
38
+ emit_event("list", { title: strip_colors(title), items: items })
39
+ end
40
+
41
+ def running?
42
+ @running
43
+ end
44
+
45
+ # ------------------------------------------------------------------
46
+ # Progress & Spinners
47
+ # ------------------------------------------------------------------
48
+
49
+ def show_spinner(message)
50
+ emit_event("progress", { message: message, status: "running" })
51
+
52
+ result = yield
53
+
54
+ status = result&.dig(:success) ? "success" : "error"
55
+ emit_event("progress", {
56
+ message: result&.dig(:message) || "Done",
57
+ status: status
58
+ })
59
+
60
+ result
61
+ rescue StandardError => e
62
+ emit_event("error", { message: e.message })
63
+ raise
64
+ end
65
+
66
+ def show_progress(title)
67
+ emit_event("progress_start", { title: title })
68
+
69
+ reporter = JsonProgressReporter.new
70
+ result = yield(reporter)
71
+
72
+ if result&.dig(:error)
73
+ # Error handled by reporter or caller
74
+ else
75
+ emit_event("progress_complete", {
76
+ message: result&.dig(:message) || "Done",
77
+ result: result
78
+ })
79
+ end
80
+
81
+ result
82
+ rescue StandardError => e
83
+ emit_event("error", { message: e.message })
84
+ raise
85
+ end
86
+
87
+ # ------------------------------------------------------------------
88
+ # Results Display
89
+ # ------------------------------------------------------------------
90
+
91
+ def show_audit_results(audit_result, _audit_type)
92
+ emit_event("result", audit_result)
93
+ end
94
+
95
+ def show_test_results(title, passed, failed, errors, total, results = [])
96
+ emit_event("test_results", {
97
+ title: title,
98
+ passed: passed,
99
+ failed: failed,
100
+ errors: errors,
101
+ total: total,
102
+ results: results
103
+ })
104
+ end
105
+
106
+ # ------------------------------------------------------------------
107
+ # Errors & Warnings
108
+ # ------------------------------------------------------------------
109
+
110
+ def show_auth_error(message = "Authentication failed")
111
+ emit_event("auth_error", { message: message })
112
+ end
113
+
114
+ def show_config_error(missing)
115
+ emit_event("config_error", { missing: missing })
116
+ end
117
+
118
+ def show_config_missing(missing_items)
119
+ emit_event("config_missing", { missing: missing_items })
120
+ end
121
+
122
+ def show_system_status(status)
123
+ emit_event("system_status", status)
124
+ end
125
+
126
+ # ------------------------------------------------------------------
127
+ # Stubs for Interactive Methods (Not supported in JSON mode)
128
+ # ------------------------------------------------------------------
129
+
130
+ def show_menu
131
+ # JSON mode is non-interactive usually, but if called, we just exit
132
+ "exit"
133
+ end
134
+
135
+ def show_test_type_menu(_mode = "test")
136
+ "back"
137
+ end
138
+
139
+ def show_list_view(_title, _items)
140
+ "back"
141
+ end
142
+
143
+ def show_post_generation_menu(_file_path, _run_command)
144
+ "back"
145
+ end
146
+
147
+ def show_auth_warning(missing_items = "")
148
+ emit_event("auth_warning", { missing: missing_items })
149
+ # In headless mode, we probably can't prompt, so we might default to cancel
150
+ # or we could make this configurable. For now, cancel to be safe.
151
+ "cancel"
152
+ end
153
+
154
+ def show_banner(_version); end
155
+ def show_goodbye; end
156
+ def show_post_install(_version); end
157
+ def show_help(_version); end
158
+ def show_stats(_stats); end
159
+ def show_clipboard_success(_cmd); end
160
+
161
+ def show_no_items(type)
162
+ emit_event("no_items", { type: type })
163
+ end
164
+
165
+ private
166
+
167
+ def strip_colors(str)
168
+ str.gsub(/\e\[\d+(;\d+)*m/, "")
169
+ end
170
+
171
+ def emit_event(type, data = {})
172
+ puts JSON.generate({ type: type }.merge(data))
173
+ end
174
+ end
175
+
176
+ class JsonProgressReporter
177
+ def initialize
178
+ @step = 0
179
+ end
180
+
181
+ def update(message, percent = nil, step_increment: true, explicit_step: nil)
182
+ step_idx = explicit_step || @step
183
+
184
+ payload = { type: "progress_update", message: message }
185
+ payload[:percent] = percent if percent
186
+ payload[:step] = step_idx
187
+
188
+ puts JSON.generate(payload)
189
+
190
+ @step += 1 if step_increment && explicit_step.nil?
191
+ end
192
+
193
+ def error(message)
194
+ puts JSON.generate({ type: "error", message: message })
195
+ end
196
+ end
197
+ end
198
+ 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.3.9"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/tng.rb CHANGED
@@ -51,9 +51,13 @@ begin
51
51
  simple_binary_paths.each do |path|
52
52
  next unless File.exist?(path)
53
53
 
54
- require path
55
- loaded = true
56
- break
54
+ begin
55
+ require path
56
+ loaded = true
57
+ break
58
+ rescue LoadError
59
+ # If this fails (e.g. wrong architecture), silently continue to try platform-specific ones
60
+ end
57
61
  end
58
62
 
59
63
  # If not loaded, try platform-specific binaries (copy to temp first)
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.3.9
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ralucab
@@ -162,6 +162,8 @@ files:
162
162
  - binaries/go-ui-darwin-arm64
163
163
  - binaries/go-ui-linux-amd64
164
164
  - binaries/go-ui-linux-arm64
165
+ - binaries/tng-darwin-arm64.bundle
166
+ - binaries/tng-darwin-x86_64.bundle
165
167
  - binaries/tng-linux-arm64.so
166
168
  - binaries/tng-linux-x86_64.so
167
169
  - binaries/tng.bundle
@@ -182,6 +184,7 @@ files:
182
184
  - lib/tng/services/testng.rb
183
185
  - lib/tng/services/user_app_config.rb
184
186
  - lib/tng/ui/go_ui_session.rb
187
+ - lib/tng/ui/json_session.rb
185
188
  - lib/tng/ui/post_install_box.rb
186
189
  - lib/tng/ui/theme.rb
187
190
  - lib/tng/utils.rb
@@ -219,7 +222,7 @@ post_install_message: "┌ TNG ────────────────
219
222
  \ │\n│ • bundle exec
220
223
  tng --help - Show help information │\n│ │\n│
221
224
  \ \U0001F4A1 Generate tests for individual methods with precision │\n└────────────────────────────────────────────────────────────
222
- v0.3.9 ┘\n"
225
+ v0.4.0 ┘\n"
223
226
  rdoc_options: []
224
227
  require_paths:
225
228
  - lib