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,102 @@
|
|
1
|
+
require "httpx"
|
2
|
+
|
3
|
+
module Tng
|
4
|
+
class HttpClient
|
5
|
+
def initialize(api_endpoint, api_key)
|
6
|
+
@api_endpoint = api_endpoint
|
7
|
+
@api_key = api_key
|
8
|
+
@timeout = {
|
9
|
+
connect_timeout: 300,
|
10
|
+
read_timeout: 300,
|
11
|
+
write_timeout: 300,
|
12
|
+
request_timeout: 300
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def post(path, payload: {}, headers: {})
|
17
|
+
default_headers = {
|
18
|
+
"Content-Type" => "application/json",
|
19
|
+
"Authorization" => "Bearer #{@api_key}",
|
20
|
+
"User-Agent" => "TestNG-Rails/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
|
21
|
+
}
|
22
|
+
|
23
|
+
merged_headers = default_headers.merge(headers)
|
24
|
+
|
25
|
+
response = HTTPX.with(timeout: @timeout).post(
|
26
|
+
"#{@api_endpoint}/#{path}",
|
27
|
+
json: payload,
|
28
|
+
headers: merged_headers
|
29
|
+
)
|
30
|
+
|
31
|
+
debug_response("POST #{path}", response) if debug_enabled?
|
32
|
+
response
|
33
|
+
end
|
34
|
+
|
35
|
+
def post_binary(path, data, headers: {})
|
36
|
+
default_headers = {
|
37
|
+
"Content-Type" => "application/octet-stream",
|
38
|
+
"Authorization" => "Bearer #{@api_key}",
|
39
|
+
"User-Agent" => "TestNG-Rails/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
|
40
|
+
}
|
41
|
+
|
42
|
+
merged_headers = default_headers.merge(headers)
|
43
|
+
|
44
|
+
response = HTTPX.with(timeout: @timeout).post(
|
45
|
+
"#{@api_endpoint}/#{path}",
|
46
|
+
body: data,
|
47
|
+
headers: merged_headers
|
48
|
+
)
|
49
|
+
|
50
|
+
debug_response("POST #{path} (binary)", response) if debug_enabled?
|
51
|
+
response
|
52
|
+
end
|
53
|
+
|
54
|
+
def get(path, headers: {})
|
55
|
+
default_headers = {
|
56
|
+
"Content-Type" => "application/json",
|
57
|
+
"Authorization" => "Bearer #{@api_key}",
|
58
|
+
"User-Agent" => "TestNG-Rails/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
|
59
|
+
}
|
60
|
+
|
61
|
+
merged_headers = default_headers.merge(headers)
|
62
|
+
|
63
|
+
response = HTTPX.with(timeout: @timeout).get(
|
64
|
+
"#{@api_endpoint}/#{path}",
|
65
|
+
headers: merged_headers
|
66
|
+
)
|
67
|
+
|
68
|
+
debug_response("GET #{path}", response) if debug_enabled?
|
69
|
+
response
|
70
|
+
end
|
71
|
+
|
72
|
+
def ping
|
73
|
+
response = HTTPX.with(timeout: @timeout).get("#{@api_endpoint}/ping")
|
74
|
+
debug_response("GET /ping", response) if debug_enabled?
|
75
|
+
response
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def debug_enabled?
|
81
|
+
ENV["DEBUG"] == "1"
|
82
|
+
end
|
83
|
+
|
84
|
+
def debug_response(request_info, response)
|
85
|
+
puts "\n -> DEBUG: #{request_info}"
|
86
|
+
puts " Status: #{response.status}"
|
87
|
+
puts " Headers: #{response.headers.to_h}"
|
88
|
+
|
89
|
+
if response.is_a?(HTTPX::ErrorResponse)
|
90
|
+
puts " Error: #{response.error&.message}"
|
91
|
+
else
|
92
|
+
body = response.body.to_s
|
93
|
+
if body.length > 500
|
94
|
+
puts " Body: #{body[0..500]}... (truncated, total length: #{body.length})"
|
95
|
+
else
|
96
|
+
puts " Body: #{body}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
puts " " + "─" * 50
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/tng/railtie.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zlib"
|
4
|
+
require "tng/utils"
|
5
|
+
require "tng/services/user_app_config"
|
6
|
+
|
7
|
+
module Tng
|
8
|
+
module Services
|
9
|
+
class TestGenerator
|
10
|
+
GENERATE_TESTS_PATH = "cli/tng_rails/contents/generate_tests"
|
11
|
+
|
12
|
+
def initialize(http_client)
|
13
|
+
@http_client = http_client
|
14
|
+
@machine_info = Tng.machine_info
|
15
|
+
end
|
16
|
+
|
17
|
+
def run_for_controller(controller)
|
18
|
+
payload = {
|
19
|
+
controller_test: Tng::Analyzers::Controller.read_test_file_for_controller(controller[:path]),
|
20
|
+
object_fixture_data: Tng::Utils.fixture_content,
|
21
|
+
machine_info: @machine_info,
|
22
|
+
ast: Tng::Analyzers::Controller.value_for_controller(controller[:path]),
|
23
|
+
test_type: "controller",
|
24
|
+
controller: controller,
|
25
|
+
routes: Tng::Analyzers::Controller.routes_for_controller(controller[:path]),
|
26
|
+
auth_patterns: Tng::Services::UserAppConfig.config_with_source,
|
27
|
+
model_info: Tng::Analyzers::Controller.model_info_for_controller(controller[:path]),
|
28
|
+
parents: Tng::Analyzers::Controller.parents_for_controller(controller)
|
29
|
+
}
|
30
|
+
|
31
|
+
send_request_and_save_test(payload)
|
32
|
+
end
|
33
|
+
|
34
|
+
def run_for_model(model)
|
35
|
+
payload = {
|
36
|
+
model_test: Tng::Analyzers::Model.read_test_file_for_model(model[:path]),
|
37
|
+
machine_info: @machine_info,
|
38
|
+
object_fixture_data: Tng::Utils.fixture_content,
|
39
|
+
ast: Tng::Analyzers::Model.value_for_model(model[:path]),
|
40
|
+
test_type: "model",
|
41
|
+
model: model,
|
42
|
+
auth_patterns: Tng::Services::UserAppConfig.config_with_source,
|
43
|
+
model_connections: Tng::Analyzers::Model.model_connections(model)
|
44
|
+
}
|
45
|
+
|
46
|
+
send_request_and_save_test(payload)
|
47
|
+
end
|
48
|
+
|
49
|
+
def run_for_service(service)
|
50
|
+
payload = {
|
51
|
+
service_test: Tng::Analyzers::Service.read_test_file_for_service(service[:path]),
|
52
|
+
machine_info: @machine_info,
|
53
|
+
object_fixture_data: Tng::Utils.fixture_content,
|
54
|
+
ast: Tng::Analyzers::Service.value_for_service(service[:path]),
|
55
|
+
test_type: "service",
|
56
|
+
service: service
|
57
|
+
}
|
58
|
+
send_request_and_save_test(payload)
|
59
|
+
end
|
60
|
+
|
61
|
+
def run_for_controller_method(controller, method_info)
|
62
|
+
payload = {
|
63
|
+
controller_test: Tng::Analyzers::Controller.read_test_file_for_controller(controller[:path]),
|
64
|
+
object_fixture_data: Tng::Utils.fixture_content,
|
65
|
+
machine_info: @machine_info,
|
66
|
+
ast: Tng::Analyzers::Controller.value_for_controller(controller[:path]),
|
67
|
+
test_type: "controller_method",
|
68
|
+
controller: controller,
|
69
|
+
method: method_info,
|
70
|
+
routes: Tng::Analyzers::Controller.routes_for_controller(controller[:path]),
|
71
|
+
auth_patterns: Tng::Services::UserAppConfig.config_with_source,
|
72
|
+
model_info: Tng::Analyzers::Controller.model_info_for_controller(controller[:path]),
|
73
|
+
parents: Tng::Analyzers::Controller.parents_for_controller(controller)
|
74
|
+
}
|
75
|
+
|
76
|
+
send_request_and_save_test(payload)
|
77
|
+
end
|
78
|
+
|
79
|
+
def run_for_model_method(model, method_info)
|
80
|
+
payload = {
|
81
|
+
model_test: Tng::Analyzers::Model.read_test_file_for_model(model[:path]),
|
82
|
+
machine_info: @machine_info,
|
83
|
+
object_fixture_data: Tng::Utils.fixture_content,
|
84
|
+
ast: Tng::Analyzers::Model.value_for_model(model[:path]),
|
85
|
+
test_type: "model_method",
|
86
|
+
model: model,
|
87
|
+
method: method_info,
|
88
|
+
auth_patterns: Tng::Services::UserAppConfig.config_with_source,
|
89
|
+
model_connections: Tng::Analyzers::Model.model_connections(model)
|
90
|
+
}
|
91
|
+
|
92
|
+
send_request_and_save_test(payload)
|
93
|
+
end
|
94
|
+
|
95
|
+
def run_for_service_method(service, method_info)
|
96
|
+
payload = {
|
97
|
+
service_test: Tng::Analyzers::Service.read_test_file_for_service(service[:path]),
|
98
|
+
machine_info: @machine_info,
|
99
|
+
object_fixture_data: Tng::Utils.fixture_content,
|
100
|
+
ast: Tng::Analyzers::Service.value_for_service(service[:path]),
|
101
|
+
test_type: "service_method",
|
102
|
+
service: service,
|
103
|
+
method: method_info,
|
104
|
+
auth_patterns: Tng::Services::UserAppConfig.config_with_source
|
105
|
+
}
|
106
|
+
|
107
|
+
send_request_and_save_test(payload)
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def send_request_and_save_test(payload)
|
113
|
+
marshaled = Marshal.dump(payload)
|
114
|
+
compressed = Zlib::Deflate.deflate(marshaled)
|
115
|
+
response = @http_client.post_binary(GENERATE_TESTS_PATH, compressed)
|
116
|
+
|
117
|
+
if response.is_a?(HTTPX::ErrorResponse)
|
118
|
+
debug_log("Request failed with error response") if debug_enabled?
|
119
|
+
return handle_request_error(response)
|
120
|
+
end
|
121
|
+
|
122
|
+
debug_log("Request successful, status: #{response.status}") if debug_enabled?
|
123
|
+
|
124
|
+
test_content = response.body.to_s
|
125
|
+
if test_content.nil? || test_content.empty?
|
126
|
+
debug_log("Empty response body received") if debug_enabled?
|
127
|
+
return handle_empty_response
|
128
|
+
end
|
129
|
+
|
130
|
+
debug_log("Received test content, length: #{test_content.length}") if debug_enabled?
|
131
|
+
Tng::Utils.save_test_file(test_content)
|
132
|
+
rescue JSON::ParserError => e
|
133
|
+
debug_log("JSON parsing failed: #{e.message}") if debug_enabled?
|
134
|
+
puts "❌ Failed to parse generated tests via API: #{e.message}"
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def handle_request_error(response)
|
139
|
+
debug_log("Handling request error: #{response.error&.message}") if debug_enabled?
|
140
|
+
puts "❌ Request failed: #{response.error&.message}"
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
|
144
|
+
def handle_empty_response
|
145
|
+
debug_log("Handling empty response") if debug_enabled?
|
146
|
+
puts "❌ Failed to generate test via API"
|
147
|
+
nil
|
148
|
+
end
|
149
|
+
|
150
|
+
def debug_enabled?
|
151
|
+
ENV["DEBUG"] == "1"
|
152
|
+
end
|
153
|
+
|
154
|
+
def debug_log(message)
|
155
|
+
puts "-> DEBUG [TestGenerator]: #{message}"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Services
|
2
|
+
class Testng
|
3
|
+
STATS_PATH = "cli/tng_rails/stats"
|
4
|
+
|
5
|
+
def initialize(http_client)
|
6
|
+
@http_client = http_client
|
7
|
+
end
|
8
|
+
|
9
|
+
def check_system_status
|
10
|
+
response = @http_client.ping
|
11
|
+
|
12
|
+
if response.is_a?(HTTPX::ErrorResponse)
|
13
|
+
return {
|
14
|
+
status: :error,
|
15
|
+
message: "Unable to connect to TNG service: #{response.error&.message}",
|
16
|
+
error_type: :connection_error
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
case response.status
|
21
|
+
when 200
|
22
|
+
begin
|
23
|
+
data = JSON.parse(response.body)
|
24
|
+
current_version = data["current_version"]
|
25
|
+
server_base_url = data["base_url"]
|
26
|
+
user_base_url = Tng::Services::UserAppConfig.base_url
|
27
|
+
|
28
|
+
if current_version != Tng::VERSION
|
29
|
+
return {
|
30
|
+
status: :version_mismatch,
|
31
|
+
message: "Version mismatch detected",
|
32
|
+
current_version: current_version,
|
33
|
+
gem_version: Tng::VERSION,
|
34
|
+
error_type: :version_mismatch
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Check for base URL mismatch
|
39
|
+
if server_base_url && user_base_url && server_base_url != user_base_url
|
40
|
+
return {
|
41
|
+
status: :base_url_mismatch,
|
42
|
+
message: "Base URL mismatch detected",
|
43
|
+
server_base_url: server_base_url,
|
44
|
+
user_base_url: user_base_url,
|
45
|
+
current_version: current_version,
|
46
|
+
gem_version: Tng::VERSION,
|
47
|
+
error_type: :base_url_mismatch
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
{
|
52
|
+
status: :ok,
|
53
|
+
message: data["message"] || "System is operational",
|
54
|
+
current_version: current_version,
|
55
|
+
gem_version: Tng::VERSION,
|
56
|
+
server_base_url: server_base_url
|
57
|
+
}
|
58
|
+
rescue JSON::ParserError => e
|
59
|
+
{
|
60
|
+
status: :error,
|
61
|
+
message: "Invalid response from server: #{e.message}",
|
62
|
+
error_type: :parse_error
|
63
|
+
}
|
64
|
+
end
|
65
|
+
else
|
66
|
+
{
|
67
|
+
status: :service_down,
|
68
|
+
message: "TestNG service is currently unavailable (HTTP #{response.status})",
|
69
|
+
error_type: :service_unavailable
|
70
|
+
}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_user_stats
|
75
|
+
headers = {
|
76
|
+
"Authorization" => "Bearer #{Tng::Services::UserAppConfig.api_key}",
|
77
|
+
"Content-Type" => "application/json",
|
78
|
+
"User-Agent" => "Tng/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
|
79
|
+
}
|
80
|
+
|
81
|
+
response = @http_client.get(STATS_PATH, headers: headers)
|
82
|
+
|
83
|
+
return nil if response.is_a?(HTTPX::ErrorResponse)
|
84
|
+
|
85
|
+
if response.status == 200
|
86
|
+
begin
|
87
|
+
stats_data = JSON.parse(response.body)
|
88
|
+
puts "✅ Statistics fetched successfully!"
|
89
|
+
stats_data
|
90
|
+
rescue JSON::ParserError => e
|
91
|
+
puts "❌ Failed to parse statistics response: #{e.message}"
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
else
|
95
|
+
puts "❌ Failed to fetch statistics (#{response.status})"
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tng
|
4
|
+
module Services
|
5
|
+
class UserAppConfig
|
6
|
+
def self.api_key
|
7
|
+
Tng.api_key
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.base_url
|
11
|
+
Tng.base_url
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.configured?
|
15
|
+
Tng.api_key && Tng.base_url
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.missing_config
|
19
|
+
missing = []
|
20
|
+
missing << "API key" unless api_key
|
21
|
+
missing << "Base URL" unless base_url
|
22
|
+
missing
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.config_with_source
|
26
|
+
auth_entry_points_with_source = []
|
27
|
+
methods = Tng.authentication_methods
|
28
|
+
methods.each do |method|
|
29
|
+
auth_entry_points_with_source << {
|
30
|
+
method: method[:method],
|
31
|
+
file: method[:file_location],
|
32
|
+
auth_type: method[:auth_type],
|
33
|
+
source: extract_method_source(method[:file_location], method[:method])
|
34
|
+
}
|
35
|
+
end
|
36
|
+
Tng.config[:authentication_entry_points_with_source] = auth_entry_points_with_source
|
37
|
+
Tng.config
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.find_method_in_ast(node, method_name)
|
41
|
+
return nil unless node.is_a?(Prism::Node)
|
42
|
+
|
43
|
+
return node if node.is_a?(Prism::DefNode) && node.name == method_name.to_sym
|
44
|
+
|
45
|
+
node.child_nodes.each do |child|
|
46
|
+
result = find_method_in_ast(child, method_name)
|
47
|
+
return result if result
|
48
|
+
end
|
49
|
+
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.extract_method_source(file_path, method_name)
|
54
|
+
return nil unless file_path && method_name
|
55
|
+
|
56
|
+
full_path = Rails.root.join(file_path)
|
57
|
+
return nil unless File.exist?(full_path)
|
58
|
+
|
59
|
+
file_content = File.read(full_path)
|
60
|
+
result = Prism.parse(file_content)
|
61
|
+
|
62
|
+
method_node = find_method_in_ast(result.value, method_name)
|
63
|
+
return nil unless method_node
|
64
|
+
|
65
|
+
# Extract the method source from the original file content
|
66
|
+
start_line = method_node.location.start_line - 1 # Convert to 0-based index
|
67
|
+
end_line = method_node.location.end_line - 1
|
68
|
+
|
69
|
+
lines = file_content.lines
|
70
|
+
method_source = lines[start_line..end_line].join
|
71
|
+
method_source.strip
|
72
|
+
rescue StandardError
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-box"
|
4
|
+
require "pastel"
|
5
|
+
require "tty-screen"
|
6
|
+
|
7
|
+
class AboutDisplay
|
8
|
+
def initialize(pastel, prompt, version)
|
9
|
+
@pastel = pastel
|
10
|
+
@prompt = prompt
|
11
|
+
@version = version
|
12
|
+
@terminal_width = begin
|
13
|
+
TTY::Screen.width
|
14
|
+
rescue StandardError
|
15
|
+
80
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def display
|
20
|
+
about_content = [
|
21
|
+
@pastel.cyan.bold("About Tng"),
|
22
|
+
"",
|
23
|
+
@pastel.bright_white("Tng is an LLM-powered test generation tool for Rails applications."),
|
24
|
+
"",
|
25
|
+
@pastel.bold("Features:"),
|
26
|
+
"• Controller test generation",
|
27
|
+
"• Model test generation",
|
28
|
+
"• LLM-powered test suggestions",
|
29
|
+
"• Test coverage analysis",
|
30
|
+
"",
|
31
|
+
@pastel.bold("Technology:"),
|
32
|
+
"• Static code analysis with Prism",
|
33
|
+
"• Intelligent pattern recognition",
|
34
|
+
"• Rails-specific optimizations",
|
35
|
+
"",
|
36
|
+
@pastel.dim("Version: #{@version}"),
|
37
|
+
@pastel.dim("Built with ❤️ for Rails developers")
|
38
|
+
].join("\n")
|
39
|
+
|
40
|
+
box_width = 64
|
41
|
+
about_box = TTY::Box.frame(
|
42
|
+
title: { top_left: " About Tng " },
|
43
|
+
style: {
|
44
|
+
fg: :bright_white,
|
45
|
+
border: { fg: :cyan },
|
46
|
+
title: { fg: :bright_cyan }
|
47
|
+
},
|
48
|
+
padding: [1, 2],
|
49
|
+
width: box_width
|
50
|
+
) do
|
51
|
+
about_content
|
52
|
+
end
|
53
|
+
|
54
|
+
puts center_box(about_box, box_width)
|
55
|
+
@prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def center_text(text)
|
61
|
+
padding = (@terminal_width - @pastel.strip(text).length) / 2
|
62
|
+
padding = 0 if padding.negative?
|
63
|
+
(" " * padding) + text
|
64
|
+
end
|
65
|
+
|
66
|
+
def center_box(box_string, box_width)
|
67
|
+
padding = (@terminal_width - box_width) / 2
|
68
|
+
padding = 0 if padding.negative?
|
69
|
+
box_string.lines.map { |line| (" " * padding) + line.chomp }.join("\n")
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pastel"
|
4
|
+
require "tty-screen"
|
5
|
+
|
6
|
+
class ConfigurationDisplay
|
7
|
+
def initialize(pastel)
|
8
|
+
@pastel = pastel
|
9
|
+
@terminal_width = begin
|
10
|
+
TTY::Screen.width
|
11
|
+
rescue StandardError
|
12
|
+
80
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def display_missing_config(missing_config)
|
17
|
+
puts @pastel.red.bold("❌ Configuration Required!")
|
18
|
+
puts
|
19
|
+
puts @pastel.yellow("Missing required configuration:")
|
20
|
+
missing_config.each do |key|
|
21
|
+
puts @pastel.cyan(" • #{key}")
|
22
|
+
end
|
23
|
+
puts
|
24
|
+
puts @pastel.bright_white("Please edit your configuration file:")
|
25
|
+
puts @pastel.cyan(" config/initializers/tng.rb")
|
26
|
+
puts
|
27
|
+
puts @pastel.bright_white("Ensure these values are set:")
|
28
|
+
puts @pastel.cyan(" Tng.configure do |config|")
|
29
|
+
puts @pastel.cyan(" config.api_key = \"your-api-key-here\"") if missing_config.include?("api_key")
|
30
|
+
puts @pastel.cyan(" config.testing_framework = \"minitest\"") if missing_config.include?("test_framework")
|
31
|
+
puts @pastel.cyan(" end")
|
32
|
+
puts
|
33
|
+
puts @pastel.dim("💡 Run 'rails generate tng:install' to create a proper config file.")
|
34
|
+
puts @pastel.dim("💡 Verify the generated setup corresponds to your project settings.")
|
35
|
+
puts
|
36
|
+
puts @pastel.yellow("After configuring, run 'bundle exec tng' again.")
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def center_text(text)
|
42
|
+
padding = (@terminal_width - @pastel.strip(text).length) / 2
|
43
|
+
padding = 0 if padding.negative?
|
44
|
+
(" " * padding) + text
|
45
|
+
end
|
46
|
+
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 ControllerTestFlowDisplay
|
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_controller(controllers)
|
20
|
+
header = @pastel.bright_white.bold("📋 Select controller 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
|
+
controllers.each do |controller|
|
31
|
+
display_name = "#{controller[:name]} #{@pastel.dim("(#{controller[:path]})")}"
|
32
|
+
menu.choice display_name, controller
|
33
|
+
end
|
34
|
+
menu.choice @pastel.cyan("⬅️ Back"), :back
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def select_test_option(controller_choice)
|
39
|
+
header = @pastel.bright_white.bold("🎯 Test Generation Options for #{controller_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_controllers_message
|
55
|
+
error_msg = "#{@pastel.red.bold("❌ No controllers found in your application")}\n#{@pastel.dim("Make sure you have controllers in app/controllers/")}"
|
56
|
+
puts center_text(error_msg)
|
57
|
+
@prompt.keypress(center_text(@pastel.dim("Press any key to continue...")))
|
58
|
+
end
|
59
|
+
|
60
|
+
def select_controller_method(controller, methods)
|
61
|
+
header = @pastel.bright_white.bold("🎯 Select method to test in #{controller[:name]}")
|
62
|
+
puts center_text(header)
|
63
|
+
|
64
|
+
@prompt.select(
|
65
|
+
"",
|
66
|
+
cycle: true,
|
67
|
+
per_page: 10,
|
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(controller)
|
79
|
+
error_msg = "#{@pastel.red.bold("❌ No methods found in #{controller[:name]}")}\n#{@pastel.dim("Controller may be empty or have syntax errors")}"
|
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
|