tng 0.1.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 +7 -0
- data/Gemfile +15 -0
- data/LICENSE.md +32 -0
- data/README.md +506 -0
- data/Rakefile +124 -0
- data/bin/load_dev +22 -0
- data/bin/tng +1096 -0
- data/binaries/tng.bundle +0 -0
- data/binaries/tng.so +0 -0
- data/lib/generators/tng/install_generator.rb +228 -0
- data/lib/tng/analyzers/controller.rb +114 -0
- data/lib/tng/analyzers/model.rb +122 -0
- data/lib/tng/analyzers/service.rb +149 -0
- data/lib/tng/api/http_client.rb +102 -0
- data/lib/tng/railtie.rb +11 -0
- data/lib/tng/services/test_generator.rb +159 -0
- data/lib/tng/services/testng.rb +100 -0
- data/lib/tng/services/user_app_config.rb +77 -0
- data/lib/tng/ui/about_display.rb +71 -0
- data/lib/tng/ui/configuration_display.rb +46 -0
- data/lib/tng/ui/controller_test_flow_display.rb +89 -0
- data/lib/tng/ui/display_banner.rb +54 -0
- data/lib/tng/ui/goodbye_display.rb +50 -0
- data/lib/tng/ui/model_test_flow_display.rb +95 -0
- data/lib/tng/ui/post_install_box.rb +73 -0
- data/lib/tng/ui/service_test_flow_display.rb +89 -0
- data/lib/tng/ui/show_help.rb +89 -0
- data/lib/tng/ui/system_status_display.rb +57 -0
- data/lib/tng/ui/user_stats_display.rb +153 -0
- data/lib/tng/utils.rb +263 -0
- data/lib/tng/version.rb +6 -0
- data/lib/tng.rb +294 -0
- data/tng.gemspec +54 -0
- metadata +283 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
require "tty-box"
|
2
|
+
require "pastel"
|
3
|
+
require "tty-screen"
|
4
|
+
|
5
|
+
class DisplayBanner
|
6
|
+
attr_reader :pastel, :version, :terminal_width
|
7
|
+
|
8
|
+
def initialize(pastel, version)
|
9
|
+
@pastel = pastel
|
10
|
+
@version = version
|
11
|
+
@terminal_width = begin
|
12
|
+
TTY::Screen.width
|
13
|
+
rescue StandardError
|
14
|
+
80
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def render
|
19
|
+
banner_content = [
|
20
|
+
pastel.cyan.bold("Tng - LLM-Powered Rails Test Generator"),
|
21
|
+
pastel.dim("Version: #{version}"),
|
22
|
+
"",
|
23
|
+
pastel.yellow("Generate comprehensive tests for your Rails application"),
|
24
|
+
pastel.dim("Powered by LLM and static analysis")
|
25
|
+
].join("\n")
|
26
|
+
|
27
|
+
box_width = 64
|
28
|
+
box_width = terminal_width if box_width > terminal_width
|
29
|
+
|
30
|
+
box = TTY::Box.frame(
|
31
|
+
title: { top_left: " Tng ", bottom_right: " Ready " },
|
32
|
+
style: {
|
33
|
+
fg: :bright_white,
|
34
|
+
border: { fg: :cyan },
|
35
|
+
title: { fg: :bright_cyan }
|
36
|
+
},
|
37
|
+
padding: [1, 2],
|
38
|
+
align: :center,
|
39
|
+
width: box_width
|
40
|
+
) do
|
41
|
+
banner_content
|
42
|
+
end
|
43
|
+
|
44
|
+
"#{center_box(box, box_width)}\n"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def center_box(box_string, box_width)
|
50
|
+
padding = (terminal_width - box_width) / 2
|
51
|
+
padding = 0 if padding.negative?
|
52
|
+
box_string.lines.map { |line| (" " * padding) + line.chomp }.join("\n")
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-box"
|
4
|
+
require "pastel"
|
5
|
+
require "tty-screen"
|
6
|
+
|
7
|
+
class GoodbyeDisplay
|
8
|
+
def initialize(pastel)
|
9
|
+
@pastel = pastel
|
10
|
+
@terminal_width = begin
|
11
|
+
TTY::Screen.width
|
12
|
+
rescue StandardError
|
13
|
+
80
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def display
|
18
|
+
box_width = 54
|
19
|
+
goodbye_box = TTY::Box.frame(
|
20
|
+
title: { top_left: " Thank You " },
|
21
|
+
style: {
|
22
|
+
fg: :bright_white,
|
23
|
+
border: { fg: :green },
|
24
|
+
title: { fg: :bright_green }
|
25
|
+
},
|
26
|
+
padding: [1, 2],
|
27
|
+
align: :center,
|
28
|
+
width: box_width
|
29
|
+
) do
|
30
|
+
[
|
31
|
+
@pastel.green.bold("Thanks for using Tng!"),
|
32
|
+
"",
|
33
|
+
@pastel.bright_white("Happy testing!"),
|
34
|
+
"",
|
35
|
+
@pastel.dim("Your Rails tests are now supercharged with LLM")
|
36
|
+
].join("\n")
|
37
|
+
end
|
38
|
+
|
39
|
+
puts center_box(goodbye_box, box_width)
|
40
|
+
puts
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def center_box(box_string, box_width)
|
46
|
+
padding = (@terminal_width - box_width) / 2
|
47
|
+
padding = 0 if padding.negative?
|
48
|
+
box_string.lines.map { |line| (" " * padding) + line.chomp }.join("\n")
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-prompt"
|
4
|
+
require "tty-spinner"
|
5
|
+
require "pastel"
|
6
|
+
require "tty-screen"
|
7
|
+
|
8
|
+
class ModelTestFlowDisplay
|
9
|
+
def initialize(prompt, pastel)
|
10
|
+
@prompt = prompt
|
11
|
+
@pastel = pastel
|
12
|
+
@terminal_width = begin
|
13
|
+
TTY::Screen.width
|
14
|
+
rescue StandardError
|
15
|
+
80
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def select_model(models)
|
20
|
+
header = @pastel.bright_white.bold("📋 Select model to test:")
|
21
|
+
puts center_text(header)
|
22
|
+
|
23
|
+
@prompt.select(
|
24
|
+
"",
|
25
|
+
cycle: true,
|
26
|
+
per_page: 12,
|
27
|
+
filter: true,
|
28
|
+
symbols: { marker: "▶" }
|
29
|
+
) do |menu|
|
30
|
+
models.each do |model|
|
31
|
+
display_name = "#{model[:name]} #{@pastel.dim("(#{model[:path]})")}"
|
32
|
+
menu.choice display_name, model
|
33
|
+
end
|
34
|
+
menu.choice @pastel.cyan("⬅️ Back"), :back
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def select_test_option(model_choice)
|
39
|
+
header = @pastel.bright_white.bold("🎯 Test Generation Options for #{model_choice[:name]}")
|
40
|
+
puts center_text(header)
|
41
|
+
puts
|
42
|
+
|
43
|
+
@prompt.select(
|
44
|
+
@pastel.bright_white("Choose test generation type:"),
|
45
|
+
cycle: true,
|
46
|
+
symbols: { marker: "▶" }
|
47
|
+
) do |menu|
|
48
|
+
menu.choice @pastel.green("Generate all possible tests"), :all_possible_tests
|
49
|
+
menu.choice @pastel.blue("Generate per method"), :per_method
|
50
|
+
menu.choice @pastel.cyan("⬅️ Back"), :back
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def show_no_models_message
|
55
|
+
puts
|
56
|
+
puts center_text(@pastel.yellow("⚠️ No models found in app/models"))
|
57
|
+
puts center_text(@pastel.dim("Make sure you're in a Rails project with models"))
|
58
|
+
puts
|
59
|
+
end
|
60
|
+
|
61
|
+
def select_model_method(model, methods)
|
62
|
+
header = @pastel.bright_white.bold("🎯 Select method to test in #{model[:name]}")
|
63
|
+
puts center_text(header)
|
64
|
+
|
65
|
+
@prompt.select(
|
66
|
+
"",
|
67
|
+
cycle: true,
|
68
|
+
per_page: 10,
|
69
|
+
filter: true,
|
70
|
+
symbols: { marker: "▶" }
|
71
|
+
) do |menu|
|
72
|
+
methods.each do |method|
|
73
|
+
menu.choice method[:name], method
|
74
|
+
end
|
75
|
+
menu.choice @pastel.cyan("⬅️ Back"), :back
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def show_no_methods_message(model)
|
80
|
+
error_msg = "#{@pastel.red.bold("❌ No methods found in #{model[:name]}")}\n#{@pastel.dim("Model may be empty or have syntax errors")}"
|
81
|
+
puts center_text(error_msg)
|
82
|
+
@prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def center_text(text)
|
88
|
+
padding = [(@terminal_width - strip_ansi(text).length) / 2, 0].max
|
89
|
+
"#{" " * padding}#{text}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def strip_ansi(text)
|
93
|
+
text.gsub(/\e\[([;\d]+)?m/, "")
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "tty-box"
|
2
|
+
require "pastel"
|
3
|
+
|
4
|
+
class PostInstallBox
|
5
|
+
attr_reader :version, :pastel
|
6
|
+
|
7
|
+
def initialize(version)
|
8
|
+
@version = version
|
9
|
+
@pastel = Pastel.new
|
10
|
+
@terminal_width = begin
|
11
|
+
TTY::Screen.width
|
12
|
+
rescue StandardError
|
13
|
+
80
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def content
|
18
|
+
[
|
19
|
+
pastel.green.bold("✅ Tng installed successfully!"),
|
20
|
+
"",
|
21
|
+
pastel.yellow.bold("📋 SETUP REQUIRED"),
|
22
|
+
"",
|
23
|
+
pastel.bright_white("1. Generate configuration:"),
|
24
|
+
pastel.cyan(" rails g tng:install"),
|
25
|
+
"",
|
26
|
+
pastel.bright_white("2. Edit configuration file:"),
|
27
|
+
pastel.cyan(" config/initializers/tng.rb"),
|
28
|
+
"",
|
29
|
+
pastel.bright_white("3. Add your license key:"),
|
30
|
+
pastel.cyan(" config.api_key = 'your-license-key-here'"),
|
31
|
+
"",
|
32
|
+
pastel.bright_white("📖 Check documentation for the correct authentication setup"),
|
33
|
+
"",
|
34
|
+
pastel.magenta.bold("🚀 Usage:"),
|
35
|
+
"",
|
36
|
+
pastel.bright_white("Interactive mode:"),
|
37
|
+
pastel.green("• bundle exec tng") + pastel.dim(" - Full interactive CLI"),
|
38
|
+
"",
|
39
|
+
pastel.bright_white("Direct generation:"),
|
40
|
+
pastel.green("• bundle exec tng -t c -f ping") + pastel.dim(" - Short"),
|
41
|
+
pastel.green("• bundle exec tng -t=c f=ping") + pastel.dim(" - Mixed"),
|
42
|
+
pastel.green("• bundle exec tng --type=controller --file=ping"),
|
43
|
+
"",
|
44
|
+
pastel.bright_white("Help:"),
|
45
|
+
pastel.green("• bundle exec tng --help") + pastel.dim(" - Show all options"),
|
46
|
+
"",
|
47
|
+
pastel.dim("💡 Use -t c for controller, -t m for model")
|
48
|
+
].join("\n")
|
49
|
+
end
|
50
|
+
|
51
|
+
def render
|
52
|
+
# Use a reasonable width that won't cause issues
|
53
|
+
box_width = [@terminal_width, 70].min
|
54
|
+
|
55
|
+
TTY::Box.frame(
|
56
|
+
title: { top_left: " TestNG ", bottom_right: " v#{@version} " },
|
57
|
+
style: {
|
58
|
+
fg: :bright_white,
|
59
|
+
border: { fg: :cyan },
|
60
|
+
title: { fg: :bright_cyan }
|
61
|
+
},
|
62
|
+
padding: [1, 2],
|
63
|
+
align: :left,
|
64
|
+
width: box_width
|
65
|
+
) do
|
66
|
+
content
|
67
|
+
end
|
68
|
+
rescue StandardError => e
|
69
|
+
"TNG v#{@version} installed successfully!\n" +
|
70
|
+
"Run 'rails g tng:install' to get started.\n" +
|
71
|
+
"Use 'bundle exec tng --help' for usage information."
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-prompt"
|
4
|
+
require "tty-spinner"
|
5
|
+
require "pastel"
|
6
|
+
require "tty-screen"
|
7
|
+
|
8
|
+
class ServiceTestFlowDisplay
|
9
|
+
def initialize(prompt, pastel)
|
10
|
+
@prompt = prompt
|
11
|
+
@pastel = pastel
|
12
|
+
@terminal_width = begin
|
13
|
+
TTY::Screen.width
|
14
|
+
rescue StandardError
|
15
|
+
80
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def select_service(services)
|
20
|
+
header = @pastel.bright_white.bold("📋 Select service to test:")
|
21
|
+
puts center_text(header)
|
22
|
+
|
23
|
+
@prompt.select(
|
24
|
+
"",
|
25
|
+
cycle: true,
|
26
|
+
per_page: 12,
|
27
|
+
filter: true,
|
28
|
+
symbols: { marker: "▶" }
|
29
|
+
) do |menu|
|
30
|
+
services.each do |service|
|
31
|
+
display_name = "#{service[:name]} #{@pastel.dim("(#{service[:path]})")}"
|
32
|
+
menu.choice display_name, service
|
33
|
+
end
|
34
|
+
menu.choice @pastel.cyan("⬅️ Back"), :back
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def select_test_option(service_choice)
|
39
|
+
header = @pastel.bright_white.bold("🎯 Test Generation Options for #{service_choice[:name]}")
|
40
|
+
puts center_text(header)
|
41
|
+
puts
|
42
|
+
|
43
|
+
@prompt.select(
|
44
|
+
@pastel.bright_white("Choose test generation type:"),
|
45
|
+
cycle: true,
|
46
|
+
symbols: { marker: "▶" }
|
47
|
+
) do |menu|
|
48
|
+
menu.choice @pastel.green("Generate all possible tests"), :all_possible_tests
|
49
|
+
menu.choice @pastel.blue("Generate per method"), :per_method
|
50
|
+
menu.choice @pastel.cyan("⬅️ Back"), :back
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def show_no_services_message
|
55
|
+
error_msg = "#{@pastel.red.bold("❌ No services found in your application")}\n#{@pastel.dim("Make sure you have services in app/services/ or app/service/")}"
|
56
|
+
puts center_text(error_msg)
|
57
|
+
@prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
|
58
|
+
end
|
59
|
+
|
60
|
+
def select_service_method(service, methods)
|
61
|
+
header = @pastel.bright_white.bold("🔍 Select method to test for #{service[:name]}")
|
62
|
+
puts center_text(header)
|
63
|
+
|
64
|
+
@prompt.select(
|
65
|
+
"",
|
66
|
+
cycle: true,
|
67
|
+
per_page: 12,
|
68
|
+
filter: true,
|
69
|
+
symbols: { marker: "▶" }
|
70
|
+
) do |menu|
|
71
|
+
methods.each do |method|
|
72
|
+
menu.choice "#{method[:name]}", method
|
73
|
+
end
|
74
|
+
menu.choice @pastel.cyan("⬅️ Back"), :back
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def show_no_methods_message(service)
|
79
|
+
error_msg = "#{@pastel.red.bold("❌ No methods found in #{service[:name]}")}\n#{@pastel.dim("The service might not have any public methods or could not be analyzed.")}"
|
80
|
+
puts center_text(error_msg)
|
81
|
+
@prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
|
82
|
+
end
|
83
|
+
|
84
|
+
def center_text(text)
|
85
|
+
padding = (@terminal_width - @pastel.strip(text).length) / 2
|
86
|
+
padding = 0 if padding.negative?
|
87
|
+
(" " * padding) + text
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "tty-box"
|
2
|
+
require "pastel"
|
3
|
+
require "tty-screen"
|
4
|
+
|
5
|
+
class ShowHelp
|
6
|
+
attr_reader :pastel
|
7
|
+
|
8
|
+
def initialize(pastel, version)
|
9
|
+
@pastel = pastel
|
10
|
+
@version = version
|
11
|
+
@terminal_width = begin
|
12
|
+
TTY::Screen.width
|
13
|
+
rescue StandardError
|
14
|
+
80
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def content
|
19
|
+
[
|
20
|
+
@pastel.cyan.bold("TestNG - LLM-Powered Rails Test Generator"),
|
21
|
+
@pastel.dim("Version: #{@version}"),
|
22
|
+
"",
|
23
|
+
@pastel.yellow.bold("Usage:"),
|
24
|
+
@pastel.bright_white(" bundle exec tng") + @pastel.dim(" # Interactive mode"),
|
25
|
+
@pastel.bright_white(" bundle exec tng --type=TYPE --file=FILE") + @pastel.dim(" # Direct mode"),
|
26
|
+
@pastel.bright_white(" bundle exec tng -t TYPE -f FILE") + @pastel.dim(" # Short aliases"),
|
27
|
+
@pastel.bright_white(" bundle exec tng -t=TYPE f=FILE") + @pastel.dim(" # Mixed format"),
|
28
|
+
"",
|
29
|
+
@pastel.yellow.bold("Examples:"),
|
30
|
+
@pastel.green(" bundle exec tng -t c -f ping") + @pastel.dim(" # Controller test"),
|
31
|
+
@pastel.green(" bundle exec tng -t=c f=ping") + @pastel.dim(" # Mixed format"),
|
32
|
+
@pastel.green(" bundle exec tng -t m -f user") + @pastel.dim(" # Model test"),
|
33
|
+
@pastel.green(" bundle exec tng --type=controller --file=users_controller"),
|
34
|
+
@pastel.green(" bundle exec tng -t c -f \"admin/users_controller\"") + @pastel.dim(" # Namespaced"),
|
35
|
+
"",
|
36
|
+
@pastel.yellow.bold("Options:"),
|
37
|
+
@pastel.bright_white(" -t, --type=TYPE") + @pastel.dim(" Test type (controller, model)"),
|
38
|
+
@pastel.bright_white(" -f, --file=FILE") + @pastel.dim(" File name (without .rb extension)"),
|
39
|
+
@pastel.bright_white(" -m, --method=METHOD") + @pastel.dim(" Method name (for per-method tests)"),
|
40
|
+
@pastel.bright_white(" -h, --help") + @pastel.dim(" Show this help message"),
|
41
|
+
"",
|
42
|
+
@pastel.yellow.bold("Type Aliases:"),
|
43
|
+
@pastel.dim(" • Controllers: ") + @pastel.cyan("c, controller"),
|
44
|
+
@pastel.dim(" • Models: ") + @pastel.cyan("m, mo, model"),
|
45
|
+
"",
|
46
|
+
@pastel.yellow.bold("File Format:"),
|
47
|
+
@pastel.dim(" • Controllers: ") + @pastel.green("users_controller, api/v1/posts_controller"),
|
48
|
+
@pastel.dim(" • Models: ") + @pastel.green("user, blog/post"),
|
49
|
+
@pastel.dim(" • Don't include .rb extension"),
|
50
|
+
"",
|
51
|
+
@pastel.magenta.bold("Pro Tips:"),
|
52
|
+
@pastel.dim(" • Mix formats: ") + @pastel.cyan("-t=c f=ping") + @pastel.dim(" works perfectly"),
|
53
|
+
@pastel.dim(" • Use filtering in interactive mode for large projects"),
|
54
|
+
@pastel.dim(" • All formats are equivalent - use what feels natural"),
|
55
|
+
"",
|
56
|
+
@pastel.cyan.bold("Happy testing! ") + @pastel.yellow("★")
|
57
|
+
].join("\n")
|
58
|
+
end
|
59
|
+
|
60
|
+
def render
|
61
|
+
# Use a reasonable width that matches other UI components
|
62
|
+
box_width = [@terminal_width - 4, 80].min
|
63
|
+
box_width = 60 if box_width < 60
|
64
|
+
|
65
|
+
box = TTY::Box.frame(
|
66
|
+
title: { top_left: " TNG Help ", bottom_right: " v#{@version} " },
|
67
|
+
style: {
|
68
|
+
fg: :bright_white,
|
69
|
+
border: { fg: :cyan },
|
70
|
+
title: { fg: :bright_cyan }
|
71
|
+
},
|
72
|
+
padding: [1, 2],
|
73
|
+
align: :left,
|
74
|
+
width: box_width
|
75
|
+
) do
|
76
|
+
content
|
77
|
+
end
|
78
|
+
|
79
|
+
center_box(box, box_width)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def center_box(box_string, box_width)
|
85
|
+
padding = (@terminal_width - box_width) / 2
|
86
|
+
padding = 0 if padding.negative?
|
87
|
+
box_string.lines.map { |line| (" " * padding) + line.chomp }.join("\n")
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pastel"
|
4
|
+
|
5
|
+
class SystemStatusDisplay
|
6
|
+
def initialize(pastel, args)
|
7
|
+
@pastel = pastel
|
8
|
+
@args = args
|
9
|
+
end
|
10
|
+
|
11
|
+
def display(status)
|
12
|
+
case status[:status]
|
13
|
+
when :ok
|
14
|
+
puts @pastel.green("✅ System operational") unless @args[:type] && @args[:file]
|
15
|
+
true
|
16
|
+
when :version_mismatch
|
17
|
+
puts @pastel.red.bold("❌ Version Mismatch!")
|
18
|
+
puts @pastel.yellow("Your gem version: #{status[:gem_version]}")
|
19
|
+
puts @pastel.yellow("Required version: #{status[:current_version]}")
|
20
|
+
puts
|
21
|
+
puts @pastel.bright_white("Please update your gem:")
|
22
|
+
puts @pastel.cyan(" gem update tng")
|
23
|
+
puts @pastel.dim(" or")
|
24
|
+
puts @pastel.cyan(" bundle update tng")
|
25
|
+
false
|
26
|
+
when :base_url_mismatch
|
27
|
+
puts @pastel.red.bold("❌ Endpoint URL Mismatch!")
|
28
|
+
puts @pastel.yellow("Your configured URL: #{status[:user_base_url]}")
|
29
|
+
puts @pastel.yellow("Server's current URL: #{status[:server_base_url]}")
|
30
|
+
puts
|
31
|
+
puts @pastel.bright_white("Please update your configuration:")
|
32
|
+
puts @pastel.cyan(" Edit config/initializers/tng.rb")
|
33
|
+
puts @pastel.cyan(" Set: config.base_url = '#{status[:server_base_url]}'")
|
34
|
+
puts
|
35
|
+
puts @pastel.dim("This ensures you're connecting to the correct endpoint.")
|
36
|
+
false
|
37
|
+
when :service_down
|
38
|
+
puts @pastel.red.bold("❌ Service Unavailable")
|
39
|
+
puts @pastel.dim(status[:message])
|
40
|
+
puts
|
41
|
+
puts @pastel.yellow("The TNG service is currently down.")
|
42
|
+
puts @pastel.dim("Please try again later or contact support.")
|
43
|
+
false
|
44
|
+
when :error
|
45
|
+
puts @pastel.red.bold("❌ Connection Error")
|
46
|
+
puts @pastel.dim(status[:message])
|
47
|
+
puts
|
48
|
+
puts @pastel.yellow("Unable to connect to TNG service.")
|
49
|
+
puts @pastel.dim("Please check your internet connection and try again.")
|
50
|
+
false
|
51
|
+
else
|
52
|
+
puts @pastel.red.bold("❌ Unknown Error")
|
53
|
+
puts @pastel.dim(status[:message] || "An unknown error occurred")
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-box"
|
4
|
+
require "tty-progressbar"
|
5
|
+
require "pastel"
|
6
|
+
require "tty-screen"
|
7
|
+
|
8
|
+
class UserStatsDisplay
|
9
|
+
def initialize(pastel, prompt)
|
10
|
+
@pastel = pastel
|
11
|
+
@prompt = prompt
|
12
|
+
@terminal_width = begin
|
13
|
+
TTY::Screen.width
|
14
|
+
rescue StandardError
|
15
|
+
80
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def display(stats_data)
|
20
|
+
header = @pastel.bright_white.bold("📊 User Statistics")
|
21
|
+
puts center_text(header)
|
22
|
+
|
23
|
+
if stats_data
|
24
|
+
display_stats_box(stats_data)
|
25
|
+
display_usage_progress(stats_data)
|
26
|
+
display_usage_info(stats_data)
|
27
|
+
else
|
28
|
+
display_error
|
29
|
+
end
|
30
|
+
|
31
|
+
puts
|
32
|
+
tip_msg = @pastel.dim("💡 Tip: Contact support if you need more test generations")
|
33
|
+
puts center_text(tip_msg)
|
34
|
+
@prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
|
35
|
+
end
|
36
|
+
|
37
|
+
def display_stats_box(stats_data)
|
38
|
+
stats_content = [
|
39
|
+
@pastel.cyan.bold("Account Information"),
|
40
|
+
"",
|
41
|
+
@pastel.bright_white("Test Runs: ") + @pastel.green.bold(stats_data["runs"].to_s),
|
42
|
+
@pastel.bright_white("Max Runs: ") + @pastel.yellow.bold(stats_data["max_runs"].to_s),
|
43
|
+
@pastel.bright_white("Gem Version: ") + @pastel.magenta(stats_data["gem_version"] || "N/A"),
|
44
|
+
"",
|
45
|
+
@pastel.dim("Request ID: #{stats_data["request_id"]}")
|
46
|
+
].join("\n")
|
47
|
+
|
48
|
+
box_width = 50
|
49
|
+
box = TTY::Box.frame(
|
50
|
+
title: { top_left: " Your TNG Stats " },
|
51
|
+
style: { fg: :bright_white, border: { fg: :green }, title: { fg: :bright_green } },
|
52
|
+
padding: [1, 2],
|
53
|
+
align: :left,
|
54
|
+
width: box_width
|
55
|
+
) { stats_content }
|
56
|
+
|
57
|
+
puts center_box(box, box_width)
|
58
|
+
end
|
59
|
+
|
60
|
+
def display_usage_progress(stats_data)
|
61
|
+
runs = stats_data["runs"]
|
62
|
+
max_runs = stats_data["max_runs"]
|
63
|
+
|
64
|
+
usage_percent = max_runs > 0 ? (runs.to_f / max_runs * 100).round(1) : 0
|
65
|
+
|
66
|
+
bar_color = determine_bar_color(usage_percent)
|
67
|
+
|
68
|
+
puts
|
69
|
+
puts center_text(@pastel.bright_white.bold("Usage Overview"))
|
70
|
+
puts
|
71
|
+
|
72
|
+
progress_width = 40
|
73
|
+
padding = (@terminal_width - progress_width - 20) / 2
|
74
|
+
padding = 0 if padding.negative?
|
75
|
+
|
76
|
+
bar_format = "#{" " * padding}Usage: [:bar] :percent (:current/:total)"
|
77
|
+
|
78
|
+
bar = TTY::ProgressBar.new(bar_format,
|
79
|
+
total: max_runs,
|
80
|
+
width: progress_width,
|
81
|
+
complete: bar_color,
|
82
|
+
incomplete: @pastel.dim("░"),
|
83
|
+
head: bar_color)
|
84
|
+
|
85
|
+
current_progress = 0
|
86
|
+
while current_progress < runs
|
87
|
+
step = [runs / 10, 1].max
|
88
|
+
current_progress = [current_progress + step, runs].min
|
89
|
+
bar.current = current_progress
|
90
|
+
bar.render
|
91
|
+
sleep(0.05) if runs > 10
|
92
|
+
end
|
93
|
+
|
94
|
+
bar.current = runs
|
95
|
+
bar.render
|
96
|
+
|
97
|
+
puts
|
98
|
+
|
99
|
+
status_msg = case usage_percent
|
100
|
+
when 0..50
|
101
|
+
@pastel.green("🟢 Good usage - plenty of runs remaining")
|
102
|
+
when 51..80
|
103
|
+
@pastel.yellow("🟡 Moderate usage - consider monitoring")
|
104
|
+
when 81..95
|
105
|
+
@pastel.cyan("🟠 High usage - approaching limit")
|
106
|
+
else
|
107
|
+
@pastel.red("🔴 Limit reached - contact support for more runs")
|
108
|
+
end
|
109
|
+
|
110
|
+
puts center_text(status_msg)
|
111
|
+
end
|
112
|
+
|
113
|
+
def determine_bar_color(usage_percent)
|
114
|
+
case usage_percent
|
115
|
+
when 0..50
|
116
|
+
@pastel.on_green(" ")
|
117
|
+
when 51..80
|
118
|
+
@pastel.on_yellow(" ")
|
119
|
+
when 81..95
|
120
|
+
@pastel.on_cyan(" ")
|
121
|
+
else
|
122
|
+
@pastel.on_red(" ")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def display_usage_info(stats_data)
|
127
|
+
usage_remaining = stats_data["max_runs"] - stats_data["runs"]
|
128
|
+
usage_msg = if usage_remaining.positive?
|
129
|
+
@pastel.green("✅ You have #{usage_remaining} test generations remaining")
|
130
|
+
else
|
131
|
+
@pastel.red("⚠️ You have reached your test generation limit")
|
132
|
+
end
|
133
|
+
puts center_text(usage_msg)
|
134
|
+
end
|
135
|
+
|
136
|
+
def display_error
|
137
|
+
error_msg = @pastel.red.bold("❌ Failed to fetch user statistics")
|
138
|
+
puts center_text(error_msg)
|
139
|
+
puts center_text(@pastel.dim("Please check your API key and internet connection"))
|
140
|
+
end
|
141
|
+
|
142
|
+
def center_text(text)
|
143
|
+
padding = (@terminal_width - @pastel.strip(text).length) / 2
|
144
|
+
padding = 0 if padding.negative?
|
145
|
+
(" " * padding) + text
|
146
|
+
end
|
147
|
+
|
148
|
+
def center_box(box_string, box_width)
|
149
|
+
padding = (@terminal_width - box_width) / 2
|
150
|
+
padding = 0 if padding.negative?
|
151
|
+
box_string.lines.map { |line| (" " * padding) + line.chomp }.join("\n")
|
152
|
+
end
|
153
|
+
end
|