tng 0.2.1
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 +7 -0
- data/Gemfile +15 -0
- data/LICENSE.md +32 -0
- data/README.md +413 -0
- data/Rakefile +124 -0
- data/bin/load_dev +22 -0
- data/bin/tng +888 -0
- data/binaries/tng.bundle +0 -0
- data/binaries/tng.so +0 -0
- data/lib/generators/tng/install_generator.rb +236 -0
- data/lib/tng/analyzers/controller.rb +114 -0
- data/lib/tng/analyzers/model.rb +131 -0
- data/lib/tng/analyzers/other.rb +277 -0
- data/lib/tng/analyzers/service.rb +150 -0
- data/lib/tng/api/http_client.rb +100 -0
- data/lib/tng/railtie.rb +11 -0
- data/lib/tng/services/direct_generation.rb +320 -0
- data/lib/tng/services/extract_methods.rb +39 -0
- data/lib/tng/services/test_generator.rb +287 -0
- data/lib/tng/services/testng.rb +100 -0
- data/lib/tng/services/user_app_config.rb +76 -0
- data/lib/tng/ui/about_display.rb +66 -0
- data/lib/tng/ui/authentication_warning_display.rb +172 -0
- data/lib/tng/ui/configuration_display.rb +52 -0
- data/lib/tng/ui/controller_test_flow_display.rb +79 -0
- data/lib/tng/ui/display_banner.rb +44 -0
- data/lib/tng/ui/goodbye_display.rb +41 -0
- data/lib/tng/ui/model_test_flow_display.rb +80 -0
- data/lib/tng/ui/other_test_flow_display.rb +78 -0
- data/lib/tng/ui/post_install_box.rb +80 -0
- data/lib/tng/ui/service_test_flow_display.rb +78 -0
- data/lib/tng/ui/show_help.rb +78 -0
- data/lib/tng/ui/system_status_display.rb +128 -0
- data/lib/tng/ui/theme.rb +258 -0
- data/lib/tng/ui/user_stats_display.rb +160 -0
- data/lib/tng/utils.rb +325 -0
- data/lib/tng/version.rb +5 -0
- data/lib/tng.rb +308 -0
- data/tng.gemspec +56 -0
- metadata +293 -0
data/lib/tng/ui/theme.rb
ADDED
@@ -0,0 +1,258 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tng
|
4
|
+
module UI
|
5
|
+
module Theme
|
6
|
+
# Icons - consistent across all UI components
|
7
|
+
ICONS = {
|
8
|
+
# Status icons
|
9
|
+
success: "✅",
|
10
|
+
error: "❌",
|
11
|
+
warning: "⚠️",
|
12
|
+
info: "ℹ️",
|
13
|
+
|
14
|
+
# Action icons
|
15
|
+
rocket: "🚀",
|
16
|
+
wave: "👋",
|
17
|
+
stats: "📊",
|
18
|
+
config: "📋",
|
19
|
+
heart: "❤️",
|
20
|
+
lightbulb: "💡",
|
21
|
+
|
22
|
+
# Navigation icons
|
23
|
+
back: "⬅️ ", # Added space after back arrow
|
24
|
+
marker: "▶",
|
25
|
+
bullet: "•"
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
# Colors - terminal-agnostic color scheme
|
29
|
+
COLORS = {
|
30
|
+
# Primary status colors
|
31
|
+
success: :green,
|
32
|
+
error: :red,
|
33
|
+
warning: :yellow,
|
34
|
+
info: :cyan,
|
35
|
+
|
36
|
+
# Text hierarchy colors
|
37
|
+
primary: :bright_white,
|
38
|
+
secondary: :cyan,
|
39
|
+
accent: :yellow,
|
40
|
+
muted: :dim,
|
41
|
+
|
42
|
+
# Border colors for boxes
|
43
|
+
border_success: :green,
|
44
|
+
border_error: :red,
|
45
|
+
border_warning: :yellow,
|
46
|
+
border_info: :cyan,
|
47
|
+
border_primary: :cyan
|
48
|
+
}.freeze
|
49
|
+
|
50
|
+
# Box styling constants
|
51
|
+
BOX_STYLES = {
|
52
|
+
default: {
|
53
|
+
style: {
|
54
|
+
border: {
|
55
|
+
type: :light,
|
56
|
+
top_left: "┌",
|
57
|
+
top_right: "┐",
|
58
|
+
bottom_left: "└",
|
59
|
+
bottom_right: "┘",
|
60
|
+
top: "─",
|
61
|
+
bottom: "─",
|
62
|
+
left: "│",
|
63
|
+
right: "│"
|
64
|
+
}
|
65
|
+
},
|
66
|
+
padding: [1, 2],
|
67
|
+
max_width: 100
|
68
|
+
},
|
69
|
+
success: {
|
70
|
+
style: {
|
71
|
+
border: {
|
72
|
+
type: :light,
|
73
|
+
top_left: "┌",
|
74
|
+
top_right: "┐",
|
75
|
+
bottom_left: "└",
|
76
|
+
bottom_right: "┘",
|
77
|
+
top: "─",
|
78
|
+
bottom: "─",
|
79
|
+
left: "│",
|
80
|
+
right: "│"
|
81
|
+
},
|
82
|
+
fg: :green
|
83
|
+
},
|
84
|
+
padding: [1, 2],
|
85
|
+
max_width: 100
|
86
|
+
},
|
87
|
+
warning: {
|
88
|
+
style: {
|
89
|
+
border: {
|
90
|
+
type: :light,
|
91
|
+
top_left: "┌",
|
92
|
+
top_right: "┐",
|
93
|
+
bottom_left: "└",
|
94
|
+
bottom_right: "┘",
|
95
|
+
top: "─",
|
96
|
+
bottom: "─",
|
97
|
+
left: "│",
|
98
|
+
right: "│"
|
99
|
+
},
|
100
|
+
fg: :yellow
|
101
|
+
},
|
102
|
+
padding: [1, 2],
|
103
|
+
max_width: 100
|
104
|
+
}
|
105
|
+
}.freeze
|
106
|
+
|
107
|
+
module_function
|
108
|
+
|
109
|
+
# Cache for background detection to avoid multiple escape sequences
|
110
|
+
@background_cache = nil
|
111
|
+
|
112
|
+
def detect_terminal_background
|
113
|
+
# Return cached result if available
|
114
|
+
return @background_cache if @background_cache
|
115
|
+
|
116
|
+
# Get background color and cache the result
|
117
|
+
@background_cache = get_background_color
|
118
|
+
@background_cache
|
119
|
+
end
|
120
|
+
|
121
|
+
def get_background_color
|
122
|
+
return :dark unless $stdout.tty? && $stdin.tty? && interactive_session?
|
123
|
+
|
124
|
+
print "\e]11;?\e\\"
|
125
|
+
$stdout.flush
|
126
|
+
|
127
|
+
require "timeout"
|
128
|
+
begin
|
129
|
+
response = ""
|
130
|
+
Timeout.timeout(0.1) do
|
131
|
+
response = $stdin.read_nonblock(100)
|
132
|
+
end
|
133
|
+
|
134
|
+
case response
|
135
|
+
when %r{rgb:([0-9a-f]{4})/([0-9a-f]{4})/([0-9a-f]{4})}i
|
136
|
+
r, g, b = [::Regexp.last_match(1), ::Regexp.last_match(2), ::Regexp.last_match(3)].map do |hex|
|
137
|
+
hex.to_i(16) >> 8
|
138
|
+
end
|
139
|
+
brightness = (r * 0.299 + g * 0.587 + b * 0.114)
|
140
|
+
return brightness > 128 ? :light : :dark
|
141
|
+
when /rgb:([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i
|
142
|
+
r, g, b = [::Regexp.last_match(1), ::Regexp.last_match(2), ::Regexp.last_match(3)].map do |hex|
|
143
|
+
hex.to_i(16)
|
144
|
+
end
|
145
|
+
brightness = (r * 0.299 + g * 0.587 + b * 0.114)
|
146
|
+
return brightness > 128 ? :light : :dark
|
147
|
+
end
|
148
|
+
rescue IO::WaitReadable, Errno::EAGAIN
|
149
|
+
# No data available
|
150
|
+
rescue Timeout::Error, StandardError
|
151
|
+
# Fall back to environment detection
|
152
|
+
end
|
153
|
+
|
154
|
+
# Enhanced fallback detection
|
155
|
+
case ENV["TERM_PROGRAM"]
|
156
|
+
when "iTerm.app"
|
157
|
+
ENV["ITERM_PROFILE"]&.downcase&.include?("light") ? :light : :dark
|
158
|
+
when "Apple_Terminal"
|
159
|
+
:light # Terminal.app defaults to light
|
160
|
+
when "vscode"
|
161
|
+
:dark # VS Code integrated terminal usually dark
|
162
|
+
else
|
163
|
+
# Check for common dark theme indicators
|
164
|
+
if ENV["COLORTERM"] == "truecolor" || ENV["TERM"]&.include?("256")
|
165
|
+
:dark
|
166
|
+
else
|
167
|
+
:light
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def interactive_session?
|
173
|
+
return false if defined?(Rails) && Rails.respond_to?(:application) && Rails.application&.initialized?
|
174
|
+
return false if ENV["BUNDLE_GEMFILE"]
|
175
|
+
return false if $PROGRAM_NAME&.include?("bundle")
|
176
|
+
return false if $PROGRAM_NAME&.include?("rails") && ARGV.any? { |arg| %w[server console runner].include?(arg) }
|
177
|
+
|
178
|
+
ENV["TNG_CLI"] == "true" || $PROGRAM_NAME&.include?("tng")
|
179
|
+
end
|
180
|
+
|
181
|
+
def adaptive_color(color_key)
|
182
|
+
base_color = base_color(color_key)
|
183
|
+
|
184
|
+
# Adjust colors based on terminal background for optimal visibility
|
185
|
+
if detect_terminal_background == :light
|
186
|
+
# Light terminal adjustments
|
187
|
+
case base_color
|
188
|
+
when :dim
|
189
|
+
:black
|
190
|
+
when :bright_white
|
191
|
+
:black # White text invisible on light backgrounds
|
192
|
+
when :cyan
|
193
|
+
:blue # Cyan can be hard to read on light backgrounds
|
194
|
+
when :yellow
|
195
|
+
:red # Yellow invisible on light backgrounds
|
196
|
+
else
|
197
|
+
base_color
|
198
|
+
end
|
199
|
+
else
|
200
|
+
# Dark terminal adjustments - optimized for macOS Terminal
|
201
|
+
case base_color
|
202
|
+
when :dim
|
203
|
+
# macOS Terminal renders :white better than :bright_white
|
204
|
+
:white
|
205
|
+
when :black
|
206
|
+
:white # Black text invisible on dark backgrounds
|
207
|
+
when :cyan
|
208
|
+
# macOS Terminal cyan can be hard to read, use blue
|
209
|
+
:blue
|
210
|
+
when :yellow
|
211
|
+
# macOS Terminal yellow can be faded, use brighter alternatives
|
212
|
+
:red
|
213
|
+
when :bright_white
|
214
|
+
# Force to regular white for better macOS Terminal compatibility
|
215
|
+
:white
|
216
|
+
else
|
217
|
+
base_color
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def icon(name)
|
223
|
+
ICONS[name] || ""
|
224
|
+
end
|
225
|
+
|
226
|
+
def color(name)
|
227
|
+
adaptive_color(name)
|
228
|
+
end
|
229
|
+
|
230
|
+
def base_color(name)
|
231
|
+
COLORS[name] || :white
|
232
|
+
end
|
233
|
+
|
234
|
+
def box_style(name)
|
235
|
+
BOX_STYLES[name] || BOX_STYLES[:default]
|
236
|
+
end
|
237
|
+
|
238
|
+
def calculate_box_width(terminal_width)
|
239
|
+
[terminal_width - 4, BOX_STYLES[:default][:max_width]].min
|
240
|
+
end
|
241
|
+
|
242
|
+
def center_box(box_content, box_width, terminal_width)
|
243
|
+
lines = box_content.split("\n")
|
244
|
+
padding = (terminal_width - box_width) / 2
|
245
|
+
padding = 0 if padding.negative?
|
246
|
+
lines.map { |line| (" " * padding) + line }.join("\n")
|
247
|
+
end
|
248
|
+
|
249
|
+
def center_text(text, terminal_width)
|
250
|
+
# Remove ANSI color codes for length calculation
|
251
|
+
clean_text = text.gsub(/\e\[[0-9;]*m/, "")
|
252
|
+
padding = (terminal_width - clean_text.length) / 2
|
253
|
+
padding = 0 if padding.negative?
|
254
|
+
(" " * padding) + text
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-box"
|
4
|
+
require "tty-progressbar"
|
5
|
+
require "pastel"
|
6
|
+
require "tty-screen"
|
7
|
+
require_relative "theme"
|
8
|
+
|
9
|
+
class UserStatsDisplay
|
10
|
+
def initialize(pastel, prompt)
|
11
|
+
@pastel = pastel
|
12
|
+
@prompt = prompt
|
13
|
+
@terminal_width = begin
|
14
|
+
TTY::Screen.width
|
15
|
+
rescue StandardError
|
16
|
+
80
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def display(stats_data)
|
21
|
+
header = @pastel.public_send(Tng::UI::Theme.color(:primary)).bold("#{Tng::UI::Theme.icon(:stats)} User Statistics")
|
22
|
+
puts Tng::UI::Theme.center_text(header, @terminal_width)
|
23
|
+
|
24
|
+
if stats_data
|
25
|
+
display_stats_box(stats_data)
|
26
|
+
display_usage_progress(stats_data)
|
27
|
+
display_usage_info(stats_data)
|
28
|
+
else
|
29
|
+
display_error
|
30
|
+
end
|
31
|
+
|
32
|
+
puts
|
33
|
+
tip_msg = @pastel.public_send(Tng::UI::Theme.color(:muted),
|
34
|
+
"#{Tng::UI::Theme.icon(:lightbulb)} Tip: Contact support if you need more test generations")
|
35
|
+
puts Tng::UI::Theme.center_text(tip_msg, @terminal_width)
|
36
|
+
@prompt.keypress(Tng::UI::Theme.center_text(
|
37
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted),
|
38
|
+
"Press any key to continue..."), @terminal_width
|
39
|
+
))
|
40
|
+
end
|
41
|
+
|
42
|
+
def display_stats_box(stats_data)
|
43
|
+
stats_content = [
|
44
|
+
@pastel.public_send(Tng::UI::Theme.color(:secondary)).bold("Account Information"),
|
45
|
+
"",
|
46
|
+
@pastel.public_send(Tng::UI::Theme.color(:primary), "Test Runs: ") + @pastel.public_send(Tng::UI::Theme.color(:success)).bold(stats_data["runs"].to_s),
|
47
|
+
@pastel.public_send(Tng::UI::Theme.color(:primary), "Max Runs: ") + @pastel.public_send(Tng::UI::Theme.color(:warning)).bold(stats_data["max_runs"].to_s),
|
48
|
+
@pastel.public_send(Tng::UI::Theme.color(:primary),
|
49
|
+
"Gem Version: ") + @pastel.public_send(Tng::UI::Theme.color(:accent),
|
50
|
+
stats_data["gem_version"] || "N/A"),
|
51
|
+
"",
|
52
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted), "Request ID: #{stats_data["request_id"]}")
|
53
|
+
].join("\n")
|
54
|
+
|
55
|
+
box_width = Tng::UI::Theme.calculate_box_width(@terminal_width)
|
56
|
+
style = Tng::UI::Theme.box_style(:success)
|
57
|
+
|
58
|
+
box = TTY::Box.frame(
|
59
|
+
title: { top_left: " Your TNG Stats " },
|
60
|
+
style: style[:style],
|
61
|
+
padding: style[:padding],
|
62
|
+
align: :left,
|
63
|
+
width: box_width
|
64
|
+
) { stats_content }
|
65
|
+
|
66
|
+
puts Tng::UI::Theme.center_box(box, box_width, @terminal_width)
|
67
|
+
end
|
68
|
+
|
69
|
+
def display_usage_progress(stats_data)
|
70
|
+
runs = stats_data["runs"]
|
71
|
+
max_runs = stats_data["max_runs"]
|
72
|
+
|
73
|
+
usage_percent = max_runs > 0 ? (runs.to_f / max_runs * 100).round(1) : 0
|
74
|
+
|
75
|
+
bar_color = determine_bar_color(usage_percent)
|
76
|
+
|
77
|
+
puts
|
78
|
+
puts Tng::UI::Theme.center_text(@pastel.public_send(Tng::UI::Theme.color(:primary)).bold("Usage Overview"),
|
79
|
+
@terminal_width)
|
80
|
+
puts
|
81
|
+
|
82
|
+
progress_width = 40
|
83
|
+
padding = (@terminal_width - progress_width - 20) / 2
|
84
|
+
padding = 0 if padding.negative?
|
85
|
+
|
86
|
+
bar_format = "#{" " * padding}Usage: [:bar] :percent (:current/:total)"
|
87
|
+
|
88
|
+
bar = TTY::ProgressBar.new(bar_format,
|
89
|
+
total: max_runs,
|
90
|
+
width: progress_width,
|
91
|
+
complete: bar_color,
|
92
|
+
incomplete: @pastel.public_send(Tng::UI::Theme.color(:muted), "░"),
|
93
|
+
head: bar_color)
|
94
|
+
|
95
|
+
current_progress = 0
|
96
|
+
while current_progress < runs
|
97
|
+
step = [runs / 10, 1].max
|
98
|
+
current_progress = [current_progress + step, runs].min
|
99
|
+
bar.current = current_progress
|
100
|
+
bar.render
|
101
|
+
sleep(0.05) if runs > 10
|
102
|
+
end
|
103
|
+
|
104
|
+
bar.current = runs
|
105
|
+
bar.render
|
106
|
+
|
107
|
+
puts
|
108
|
+
|
109
|
+
status_msg = case usage_percent
|
110
|
+
when 0..50
|
111
|
+
@pastel.public_send(Tng::UI::Theme.color(:success),
|
112
|
+
"#{Tng::UI::Theme.icon(:success)} Good usage - plenty of runs remaining")
|
113
|
+
when 51..80
|
114
|
+
@pastel.public_send(Tng::UI::Theme.color(:warning),
|
115
|
+
"#{Tng::UI::Theme.icon(:warning)} Moderate usage - consider monitoring")
|
116
|
+
when 81..95
|
117
|
+
@pastel.public_send(Tng::UI::Theme.color(:info),
|
118
|
+
"#{Tng::UI::Theme.icon(:warning)} High usage - approaching limit")
|
119
|
+
else
|
120
|
+
@pastel.public_send(Tng::UI::Theme.color(:error),
|
121
|
+
"#{Tng::UI::Theme.icon(:error)} Limit reached - contact support for more runs")
|
122
|
+
end
|
123
|
+
|
124
|
+
puts Tng::UI::Theme.center_text(status_msg, @terminal_width)
|
125
|
+
end
|
126
|
+
|
127
|
+
def determine_bar_color(usage_percent)
|
128
|
+
case usage_percent
|
129
|
+
when 0..50
|
130
|
+
@pastel.public_send(Tng::UI::Theme.color(:success), "█")
|
131
|
+
when 51..80
|
132
|
+
@pastel.public_send(Tng::UI::Theme.color(:warning), "█")
|
133
|
+
when 81..95
|
134
|
+
@pastel.public_send(Tng::UI::Theme.color(:info), "█")
|
135
|
+
else
|
136
|
+
@pastel.public_send(Tng::UI::Theme.color(:error), "█")
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def display_usage_info(stats_data)
|
141
|
+
usage_remaining = stats_data["max_runs"] - stats_data["runs"]
|
142
|
+
usage_msg = if usage_remaining.positive?
|
143
|
+
@pastel.public_send(Tng::UI::Theme.color(:success),
|
144
|
+
"#{Tng::UI::Theme.icon(:success)} You have #{usage_remaining} test generations remaining")
|
145
|
+
else
|
146
|
+
@pastel.public_send(Tng::UI::Theme.color(:error),
|
147
|
+
"#{Tng::UI::Theme.icon(:warning)} You have reached your test generation limit")
|
148
|
+
end
|
149
|
+
puts Tng::UI::Theme.center_text(usage_msg, @terminal_width)
|
150
|
+
end
|
151
|
+
|
152
|
+
def display_error
|
153
|
+
error_msg = @pastel.public_send(Tng::UI::Theme.color(:error)).bold("#{Tng::UI::Theme.icon(:error)} Failed to fetch user statistics")
|
154
|
+
puts Tng::UI::Theme.center_text(error_msg, @terminal_width)
|
155
|
+
puts Tng::UI::Theme.center_text(
|
156
|
+
@pastel.public_send(Tng::UI::Theme.color(:muted),
|
157
|
+
"Please check your API key and internet connection"), @terminal_width
|
158
|
+
)
|
159
|
+
end
|
160
|
+
end
|