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
@@ -0,0 +1,277 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tng
|
4
|
+
module Analyzers
|
5
|
+
class Other
|
6
|
+
# Directories to scan for "other" files
|
7
|
+
OTHER_DIRECTORIES = %w[
|
8
|
+
app/jobs
|
9
|
+
app/helpers
|
10
|
+
lib
|
11
|
+
libs
|
12
|
+
app/lib
|
13
|
+
app/libs
|
14
|
+
app/mailers
|
15
|
+
app/channels
|
16
|
+
app/decorators
|
17
|
+
app/presenters
|
18
|
+
app/serializers
|
19
|
+
app/policies
|
20
|
+
app/forms
|
21
|
+
app/queries
|
22
|
+
app/graphql
|
23
|
+
app/graphql/resolvers
|
24
|
+
app/graphql/types
|
25
|
+
app/graphql/mutations
|
26
|
+
app/graphql/loaders
|
27
|
+
app/graphql/schemas
|
28
|
+
].freeze
|
29
|
+
|
30
|
+
def self.files_in_dir(dir = nil)
|
31
|
+
base_dirs = if dir.nil?
|
32
|
+
OTHER_DIRECTORIES.select { |d| Dir.exist?(File.join(Dir.pwd, d)) }
|
33
|
+
else
|
34
|
+
[dir]
|
35
|
+
end
|
36
|
+
|
37
|
+
return [] if base_dirs.empty?
|
38
|
+
|
39
|
+
base_dirs.flat_map do |base_dir|
|
40
|
+
full_path = File.join(Dir.pwd, base_dir)
|
41
|
+
find_other_files(full_path, base_dir)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.value_for_other(file_path)
|
46
|
+
raise "file_path is required" if file_path.nil?
|
47
|
+
|
48
|
+
# For now, use the same parsing logic as services
|
49
|
+
# We can enhance this later if needed for specific file types
|
50
|
+
Tng::Analyzer::Service.parse_service_file(file_path)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.read_test_file_for_other(other_path)
|
54
|
+
files = find_test_file_for_other(other_path)
|
55
|
+
|
56
|
+
files.map do |file_path|
|
57
|
+
{
|
58
|
+
path: file_path,
|
59
|
+
content: File.read(file_path)
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.find_test_file_for_other(other_path)
|
65
|
+
# Extract the file type and name for test file discovery
|
66
|
+
file_type = determine_file_type(other_path)
|
67
|
+
file_name = File.basename(other_path, ".rb")
|
68
|
+
testing_framework = Tng.testing_framework
|
69
|
+
return [] if testing_framework.nil?
|
70
|
+
|
71
|
+
paths = if testing_framework.downcase == "rspec"
|
72
|
+
build_rspec_test_paths(file_type, file_name)
|
73
|
+
else
|
74
|
+
build_minitest_test_paths(file_type, file_name)
|
75
|
+
end
|
76
|
+
|
77
|
+
paths.select { |path| File.exist?(path) }
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.methods_for_other(other_name, file_path)
|
81
|
+
raise "other_name is required" if other_name.nil?
|
82
|
+
raise "file_path is required" if file_path.nil?
|
83
|
+
|
84
|
+
begin
|
85
|
+
# Try to load the class/module if possible
|
86
|
+
if File.exist?(file_path)
|
87
|
+
source_code = File.read(file_path)
|
88
|
+
result = Prism.parse(source_code)
|
89
|
+
|
90
|
+
public_methods = []
|
91
|
+
extract_public_method_names(result.value, public_methods, :public)
|
92
|
+
|
93
|
+
# Return the public methods found in the source file
|
94
|
+
public_methods.map { |method_name| { name: method_name.to_s } }
|
95
|
+
else
|
96
|
+
[]
|
97
|
+
end
|
98
|
+
rescue StandardError => e
|
99
|
+
puts "❌ Error analyzing file #{other_name}: #{e.message}"
|
100
|
+
[]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.extract_public_method_names(node, methods, current_visibility = :public)
|
105
|
+
return unless node.is_a?(Prism::Node)
|
106
|
+
|
107
|
+
case node
|
108
|
+
when Prism::DefNode
|
109
|
+
# Only add methods that are public
|
110
|
+
methods << node.name.to_s if current_visibility == :public
|
111
|
+
when Prism::CallNode
|
112
|
+
# Handle visibility modifiers (private, protected, public)
|
113
|
+
if node.receiver.nil? && node.arguments.nil?
|
114
|
+
case node.name
|
115
|
+
when :private
|
116
|
+
current_visibility = :private
|
117
|
+
when :protected
|
118
|
+
current_visibility = :protected
|
119
|
+
when :public
|
120
|
+
current_visibility = :public
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
when Prism::SingletonClassNode
|
125
|
+
# Methods inside class << self blocks - always public by default
|
126
|
+
node.body&.child_nodes&.each do |child|
|
127
|
+
extract_public_method_names(child, methods, :public)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Recursively process child nodes with current visibility
|
132
|
+
node.child_nodes.each do |child|
|
133
|
+
extract_public_method_names(child, methods, current_visibility)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.find_other_files(dir, _base_dir)
|
138
|
+
return [] unless Dir.exist?(dir)
|
139
|
+
|
140
|
+
Dir.glob(File.join(dir, "**", "*.rb")).map do |file_path|
|
141
|
+
relative_path = file_path.gsub("#{Dir.pwd}/", "")
|
142
|
+
file_type = determine_file_type(file_path)
|
143
|
+
|
144
|
+
{
|
145
|
+
file: File.basename(file_path),
|
146
|
+
path: file_path,
|
147
|
+
relative_path: relative_path,
|
148
|
+
type: file_type
|
149
|
+
}
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.determine_file_type(file_path)
|
154
|
+
case file_path
|
155
|
+
when %r{app/jobs}
|
156
|
+
"job"
|
157
|
+
when %r{app/helpers}
|
158
|
+
"helper"
|
159
|
+
when %r{(?:app/)?libs?(?:/|$)}
|
160
|
+
"lib"
|
161
|
+
when %r{app/mailers}
|
162
|
+
"mailer"
|
163
|
+
when %r{app/channels}
|
164
|
+
"channel"
|
165
|
+
when %r{app/decorators}
|
166
|
+
"decorator"
|
167
|
+
when %r{app/presenters}
|
168
|
+
"presenter"
|
169
|
+
when %r{app/serializers}
|
170
|
+
"serializer"
|
171
|
+
when %r{app/policies}
|
172
|
+
"policy"
|
173
|
+
when %r{app/forms}
|
174
|
+
"form"
|
175
|
+
when %r{app/queries}
|
176
|
+
"query"
|
177
|
+
when %r{app/graphql/resolvers}
|
178
|
+
"resolver"
|
179
|
+
when %r{app/graphql/types}
|
180
|
+
"type"
|
181
|
+
when %r{app/graphql/mutations}
|
182
|
+
"mutation"
|
183
|
+
when %r{app/graphql/loaders}
|
184
|
+
"loader"
|
185
|
+
when %r{app/graphql/schemas}
|
186
|
+
"schema"
|
187
|
+
when %r{app/graphql}
|
188
|
+
"graphql"
|
189
|
+
else
|
190
|
+
"other"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def self.build_rspec_test_paths(file_type, file_name)
|
195
|
+
case file_type
|
196
|
+
when "job"
|
197
|
+
["spec/jobs/#{file_name}_spec.rb"]
|
198
|
+
when "helper"
|
199
|
+
["spec/helpers/#{file_name}_spec.rb"]
|
200
|
+
when "lib"
|
201
|
+
["spec/lib/#{file_name}_spec.rb"]
|
202
|
+
when "mailer"
|
203
|
+
["spec/mailers/#{file_name}_spec.rb"]
|
204
|
+
when "channel"
|
205
|
+
["spec/channels/#{file_name}_spec.rb"]
|
206
|
+
when "query"
|
207
|
+
["spec/queries/#{file_name}_spec.rb"]
|
208
|
+
when "decorator"
|
209
|
+
["spec/decorators/#{file_name}_spec.rb"]
|
210
|
+
when "presenter"
|
211
|
+
["spec/presenters/#{file_name}_spec.rb"]
|
212
|
+
when "serializer"
|
213
|
+
["spec/serializers/#{file_name}_spec.rb"]
|
214
|
+
when "policy"
|
215
|
+
["spec/policies/#{file_name}_spec.rb"]
|
216
|
+
when "form"
|
217
|
+
["spec/forms/#{file_name}_spec.rb"]
|
218
|
+
when "resolver"
|
219
|
+
["spec/graphql/resolvers/#{file_name}_spec.rb"]
|
220
|
+
when "type"
|
221
|
+
["spec/graphql/types/#{file_name}_spec.rb"]
|
222
|
+
when "mutation"
|
223
|
+
["spec/graphql/mutations/#{file_name}_spec.rb"]
|
224
|
+
when "loader"
|
225
|
+
["spec/graphql/loaders/#{file_name}_spec.rb"]
|
226
|
+
when "schema"
|
227
|
+
["spec/graphql/schemas/#{file_name}_spec.rb"]
|
228
|
+
when "graphql"
|
229
|
+
["spec/graphql/#{file_name}_spec.rb"]
|
230
|
+
else
|
231
|
+
["spec/#{file_type}s/#{file_name}_spec.rb"]
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def self.build_minitest_test_paths(file_type, file_name)
|
236
|
+
case file_type
|
237
|
+
when "job"
|
238
|
+
["test/jobs/#{file_name}_test.rb"]
|
239
|
+
when "helper"
|
240
|
+
["test/helpers/#{file_name}_test.rb"]
|
241
|
+
when "lib"
|
242
|
+
["test/lib/#{file_name}_test.rb"]
|
243
|
+
when "mailer"
|
244
|
+
["test/mailers/#{file_name}_test.rb"]
|
245
|
+
when "channel"
|
246
|
+
["test/channels/#{file_name}_test.rb"]
|
247
|
+
when "query"
|
248
|
+
["test/queries/#{file_name}_test.rb"]
|
249
|
+
when "decorator"
|
250
|
+
["test/decorators/#{file_name}_test.rb"]
|
251
|
+
when "presenter"
|
252
|
+
["test/presenters/#{file_name}_test.rb"]
|
253
|
+
when "serializer"
|
254
|
+
["test/serializers/#{file_name}_test.rb"]
|
255
|
+
when "policy"
|
256
|
+
["test/policies/#{file_name}_test.rb"]
|
257
|
+
when "form"
|
258
|
+
["test/forms/#{file_name}_test.rb"]
|
259
|
+
when "resolver"
|
260
|
+
["test/graphql/resolvers/#{file_name}_test.rb"]
|
261
|
+
when "type"
|
262
|
+
["test/graphql/types/#{file_name}_test.rb"]
|
263
|
+
when "mutation"
|
264
|
+
["test/graphql/mutations/#{file_name}_test.rb"]
|
265
|
+
when "loader"
|
266
|
+
["test/graphql/loaders/#{file_name}_test.rb"]
|
267
|
+
when "schema"
|
268
|
+
["test/graphql/schemas/#{file_name}_test.rb"]
|
269
|
+
when "graphql"
|
270
|
+
["test/graphql/#{file_name}_test.rb"]
|
271
|
+
else
|
272
|
+
["test/#{file_type}s/#{file_name}_test.rb"]
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tng
|
4
|
+
module Analyzers
|
5
|
+
class Service
|
6
|
+
def self.files_in_dir(dir = nil)
|
7
|
+
base_dirs = if dir.nil?
|
8
|
+
["app/services", "app/service"].select { |d| Dir.exist?(File.join(Dir.pwd, d)) }
|
9
|
+
else
|
10
|
+
[dir]
|
11
|
+
end
|
12
|
+
|
13
|
+
return [] if base_dirs.empty?
|
14
|
+
|
15
|
+
base_dirs.flat_map do |base_dir|
|
16
|
+
full_path = File.join(Dir.pwd, base_dir)
|
17
|
+
find_service_files(full_path)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.value_for_service(file_path)
|
22
|
+
raise "file_path is required" if file_path.nil?
|
23
|
+
|
24
|
+
Tng::Analyzer::Service.parse_service_file(file_path)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.read_test_file_for_service(service_path)
|
28
|
+
files = find_test_file_for_service(service_path)
|
29
|
+
|
30
|
+
files.map do |file_path|
|
31
|
+
{
|
32
|
+
path: file_path,
|
33
|
+
content: File.read(file_path)
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.find_test_file_for_service(service_path)
|
39
|
+
service_name = File.basename(service_path, ".rb")
|
40
|
+
testing_framework = Tng.testing_framework
|
41
|
+
return [] if testing_framework.nil?
|
42
|
+
|
43
|
+
paths = if testing_framework.downcase == "rspec"
|
44
|
+
[
|
45
|
+
"spec/services/#{service_name}_spec.rb",
|
46
|
+
"spec/service/#{service_name}_spec.rb"
|
47
|
+
]
|
48
|
+
else
|
49
|
+
[
|
50
|
+
"test/services/#{service_name}_test.rb",
|
51
|
+
"test/service/#{service_name}_test.rb"
|
52
|
+
]
|
53
|
+
end
|
54
|
+
|
55
|
+
paths.select { |path| File.exist?(path) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.methods_for_service(service_name)
|
59
|
+
raise "service_name is required" if service_name.nil?
|
60
|
+
|
61
|
+
begin
|
62
|
+
# Load the service class
|
63
|
+
service_class = service_name.constantize
|
64
|
+
|
65
|
+
instance_methods = service_class.public_instance_methods(false)
|
66
|
+
class_methods = service_class.public_methods(false) - Class.public_methods
|
67
|
+
|
68
|
+
# Try to get source file from any method, fallback to const_source_location
|
69
|
+
service_file = nil
|
70
|
+
|
71
|
+
# First try to get file from an instance method
|
72
|
+
if instance_methods.any?
|
73
|
+
begin
|
74
|
+
service_file = service_class.instance_method(instance_methods.first).source_location&.first
|
75
|
+
rescue StandardError
|
76
|
+
# Method might not have source location, continue
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Fallback to const_source_location if no method source found
|
81
|
+
service_file ||= service_class.const_source_location(service_class.name.split("::").last)&.first
|
82
|
+
|
83
|
+
service_methods = if service_file && File.exist?(service_file)
|
84
|
+
source_code = File.read(service_file)
|
85
|
+
result = Prism.parse(source_code)
|
86
|
+
|
87
|
+
defined_methods = []
|
88
|
+
extract_method_names(result.value, defined_methods)
|
89
|
+
|
90
|
+
filtered_instance_methods = instance_methods.select do |method_name|
|
91
|
+
method = service_class.instance_method(method_name)
|
92
|
+
next false unless method.owner == service_class
|
93
|
+
|
94
|
+
defined_methods.include?(method_name.to_s)
|
95
|
+
end
|
96
|
+
|
97
|
+
filtered_class_methods = class_methods.select do |method_name|
|
98
|
+
defined_methods.include?(method_name.to_s)
|
99
|
+
end
|
100
|
+
|
101
|
+
filtered_instance_methods + filtered_class_methods
|
102
|
+
else
|
103
|
+
[]
|
104
|
+
end
|
105
|
+
|
106
|
+
service_methods.map { |method_name| { name: method_name.to_s } }
|
107
|
+
rescue NameError => e
|
108
|
+
puts "❌ Could not load service class #{service_name}: #{e.message}"
|
109
|
+
[]
|
110
|
+
rescue StandardError => e
|
111
|
+
puts "❌ Error analyzing service #{service_name}: #{e.message}"
|
112
|
+
[]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.extract_method_names(node, methods)
|
117
|
+
return unless node.is_a?(Prism::Node)
|
118
|
+
|
119
|
+
case node
|
120
|
+
when Prism::DefNode
|
121
|
+
# Both instance and class methods (def self.method_name)
|
122
|
+
methods << node.name.to_s
|
123
|
+
|
124
|
+
when Prism::SingletonClassNode
|
125
|
+
# Methods inside class << self blocks
|
126
|
+
node.body&.child_nodes&.each do |child|
|
127
|
+
extract_method_names(child, methods)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
node.child_nodes.each do |child|
|
132
|
+
extract_method_names(child, methods)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.find_service_files(dir)
|
137
|
+
return [] unless Dir.exist?(dir)
|
138
|
+
|
139
|
+
Dir.glob(File.join(dir, "**", "*.rb")).map do |file_path|
|
140
|
+
relative_path = file_path.gsub("#{Dir.pwd}/", "")
|
141
|
+
{
|
142
|
+
file: File.basename(file_path),
|
143
|
+
path: file_path,
|
144
|
+
relative_path: relative_path
|
145
|
+
}
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,100 @@
|
|
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
|
+
merged_headers = json_default_headers.merge(headers)
|
18
|
+
|
19
|
+
response = HTTPX.with(timeout: @timeout).post(
|
20
|
+
"#{@api_endpoint}/#{path}",
|
21
|
+
json: payload,
|
22
|
+
headers: merged_headers
|
23
|
+
)
|
24
|
+
|
25
|
+
debug_response("POST #{path}", response) if debug_enabled?
|
26
|
+
response
|
27
|
+
end
|
28
|
+
|
29
|
+
def post_binary(path, data, headers: {})
|
30
|
+
merged_headers = stream_default_headers.merge(headers)
|
31
|
+
|
32
|
+
response = HTTPX.with(timeout: @timeout).post(
|
33
|
+
"#{@api_endpoint}/#{path}",
|
34
|
+
body: data,
|
35
|
+
headers: merged_headers
|
36
|
+
)
|
37
|
+
|
38
|
+
debug_response("POST #{path} (binary)", response) if debug_enabled?
|
39
|
+
response
|
40
|
+
end
|
41
|
+
|
42
|
+
def get(path, headers: {})
|
43
|
+
merged_headers = json_default_headers.merge(headers)
|
44
|
+
|
45
|
+
response = HTTPX.with(timeout: @timeout).get(
|
46
|
+
"#{@api_endpoint}/#{path}",
|
47
|
+
headers: merged_headers
|
48
|
+
)
|
49
|
+
|
50
|
+
debug_response("GET #{path}", response) if debug_enabled?
|
51
|
+
response
|
52
|
+
end
|
53
|
+
|
54
|
+
def ping
|
55
|
+
response = HTTPX.with(timeout: @timeout).get("#{@api_endpoint}/ping")
|
56
|
+
debug_response("GET /ping", response) if debug_enabled?
|
57
|
+
response
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def stream_default_headers
|
63
|
+
{
|
64
|
+
"Content-Type" => "application/octet-stream",
|
65
|
+
"Authorization" => "Bearer #{@api_key}",
|
66
|
+
"User-Agent" => "TestNG-Rails/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def json_default_headers
|
71
|
+
{
|
72
|
+
"Content-Type" => "application/json",
|
73
|
+
"Authorization" => "Bearer #{@api_key}",
|
74
|
+
"User-Agent" => "TestNG-Rails/#{Tng::VERSION} Ruby/#{RUBY_VERSION}"
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def debug_enabled?
|
79
|
+
ENV["DEBUG"] == "1"
|
80
|
+
end
|
81
|
+
|
82
|
+
def debug_response(request_info, response)
|
83
|
+
puts "\n -> DEBUG: #{request_info}"
|
84
|
+
|
85
|
+
if response.is_a?(HTTPX::ErrorResponse)
|
86
|
+
puts " Error: #{response.error&.message}"
|
87
|
+
else
|
88
|
+
puts " Status: #{response.status}"
|
89
|
+
puts " Headers: #{response.headers.to_h}"
|
90
|
+
body = response.body.to_s
|
91
|
+
if body.length > 500
|
92
|
+
puts " Body: #{body[0..500]}... (truncated, total length: #{body.length})"
|
93
|
+
else
|
94
|
+
puts " Body: #{body}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
puts " " + "─" * 50
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|