kronk 1.4.0 → 1.5.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.
- data/.gemtest +0 -0
- data/History.rdoc +22 -0
- data/Manifest.txt +15 -6
- data/README.rdoc +5 -6
- data/Rakefile +5 -5
- data/lib/kronk.rb +164 -194
- data/lib/kronk/cmd.rb +188 -74
- data/lib/kronk/constants.rb +90 -0
- data/lib/kronk/data_renderer.rb +146 -0
- data/lib/kronk/diff.rb +4 -92
- data/lib/kronk/path/transaction.rb +2 -0
- data/lib/kronk/player.rb +233 -0
- data/lib/kronk/player/benchmark.rb +261 -0
- data/lib/kronk/player/input_reader.rb +54 -0
- data/lib/kronk/player/output.rb +49 -0
- data/lib/kronk/player/request_parser.rb +24 -0
- data/lib/kronk/player/stream.rb +50 -0
- data/lib/kronk/player/suite.rb +123 -0
- data/lib/kronk/plist_parser.rb +4 -0
- data/lib/kronk/request.rb +265 -241
- data/lib/kronk/response.rb +330 -149
- data/lib/kronk/test/assertions.rb +2 -2
- data/lib/kronk/xml_parser.rb +7 -1
- data/test/mocks/cookies.yml +32 -0
- data/test/mocks/get_request.txt +6 -0
- data/test/test_assertions.rb +6 -6
- data/test/test_cmd.rb +708 -0
- data/test/test_diff.rb +210 -75
- data/test/test_helper.rb +140 -0
- data/test/test_helper_methods.rb +4 -4
- data/test/test_input_reader.rb +103 -0
- data/test/test_kronk.rb +142 -141
- data/test/test_player.rb +589 -0
- data/test/test_request.rb +147 -212
- data/test/test_request_parser.rb +31 -0
- data/test/test_response.rb +206 -15
- metadata +41 -74
- data/bin/yzma +0 -13
- data/lib/kronk/data_set.rb +0 -144
- data/lib/yzma.rb +0 -174
- data/lib/yzma/randomizer.rb +0 -54
- data/lib/yzma/report.rb +0 -47
- data/test/test_data_set.rb +0 -213
data/lib/kronk/response.rb
CHANGED
@@ -1,254 +1,435 @@
|
|
1
1
|
class Kronk
|
2
2
|
|
3
3
|
##
|
4
|
-
#
|
4
|
+
# Mock File IO to allow rewinding on Windows platforms.
|
5
5
|
|
6
|
-
class
|
6
|
+
class WinFileIO < StringIO
|
7
|
+
attr_accessor :path
|
8
|
+
|
9
|
+
def initialize path, str=""
|
10
|
+
@path = path
|
11
|
+
super str
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Wraps an http response.
|
17
|
+
|
18
|
+
class Response
|
7
19
|
|
8
20
|
class MissingParser < Exception; end
|
9
21
|
|
22
|
+
ENCODING_MATCHER = /(^|;\s?)charset=(.*?)\s*(;|$)/
|
23
|
+
|
10
24
|
##
|
11
|
-
#
|
25
|
+
# Read http response from a file and return a HTTPResponse instance.
|
12
26
|
|
13
|
-
def self.
|
14
|
-
|
15
|
-
io.debug_output = socket_io = StringIO.new
|
27
|
+
def self.read_file path
|
28
|
+
Kronk::Cmd.verbose "Reading file: #{path}\n"
|
16
29
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
30
|
+
file = File.open(path, "rb")
|
31
|
+
resp = new file
|
32
|
+
resp.uri = path
|
33
|
+
file.close
|
34
|
+
|
35
|
+
resp
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
attr_accessor :body, :bytes, :byterate, :code, :headers, :parser,
|
40
|
+
:raw, :request, :uri
|
41
|
+
|
42
|
+
attr_reader :encoding, :time
|
43
|
+
|
44
|
+
alias to_hash headers
|
45
|
+
alias to_s raw
|
46
|
+
|
47
|
+
##
|
48
|
+
# Create a new Response object from a String or IO.
|
49
|
+
|
50
|
+
def initialize io=nil, res=nil, request=nil
|
51
|
+
return unless io
|
52
|
+
io = StringIO.new io if String === io
|
53
|
+
|
54
|
+
if io && res
|
55
|
+
@_res, debug_io = res, io
|
56
|
+
else
|
57
|
+
@_res, debug_io = request_from_io(io)
|
21
58
|
end
|
22
59
|
|
23
|
-
|
24
|
-
resp.set_helper_attribs socket_io, true, true
|
60
|
+
@headers = @_res.to_hash
|
25
61
|
|
26
|
-
|
62
|
+
@encoding = "utf-8" unless @_res["Content-Type"]
|
63
|
+
c_type = [*@headers["content-type"]].find{|ct| ct =~ ENCODING_MATCHER}
|
64
|
+
@encoding = $2 if c_type
|
65
|
+
@encoding ||= "ASCII-8BIT"
|
66
|
+
@encoding = Encoding.find(@encoding) if defined?(Encoding)
|
67
|
+
|
68
|
+
raw_req, raw_resp, bytes = read_raw_from debug_io
|
69
|
+
@raw = try_force_encoding raw_resp
|
70
|
+
|
71
|
+
@request = request ||
|
72
|
+
raw_req = try_force_encoding(raw_req) &&
|
73
|
+
Request.parse(raw_req)
|
74
|
+
|
75
|
+
@time = 0
|
76
|
+
|
77
|
+
@body = try_force_encoding(@_res.body) if @_res.body
|
78
|
+
@body ||= @raw.split("\r\n\r\n",2)[1]
|
79
|
+
|
80
|
+
@bytes = (@_res['Content-Length'] || @body.bytes.count).to_i
|
81
|
+
|
82
|
+
@code = @_res.code
|
83
|
+
|
84
|
+
@parser = Kronk.parser_for @_res['Content-Type']
|
85
|
+
|
86
|
+
@uri = @request.uri if @request && @request.uri
|
87
|
+
|
88
|
+
@byterate = 0
|
27
89
|
end
|
28
90
|
|
29
91
|
|
92
|
+
##
|
93
|
+
# Accessor for the HTTPResponse instance []
|
94
|
+
|
95
|
+
def [] key
|
96
|
+
@_res[key]
|
97
|
+
end
|
98
|
+
|
30
99
|
|
31
100
|
##
|
32
|
-
#
|
101
|
+
# Accessor for the HTTPResponse instance []
|
33
102
|
|
34
|
-
|
103
|
+
def []= key, value
|
104
|
+
@_res[key] = value
|
105
|
+
end
|
35
106
|
|
36
|
-
##
|
37
|
-
# Read the raw response from a debug_output instance and return an array
|
38
|
-
# containing the raw request, response, and number of bytes received.
|
39
107
|
|
40
|
-
|
41
|
-
|
42
|
-
resp = ""
|
43
|
-
bytes = nil
|
108
|
+
##
|
109
|
+
# Cookie header accessor.
|
44
110
|
|
45
|
-
|
46
|
-
|
111
|
+
def cookie
|
112
|
+
@_res['Cookie']
|
113
|
+
end
|
47
114
|
|
48
|
-
if output.first =~ %r{<-\s(.*)}
|
49
|
-
req = instance_eval $1
|
50
|
-
output.delete_at 0
|
51
|
-
end
|
52
115
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
116
|
+
##
|
117
|
+
# Force the encoding of the raw response and body
|
118
|
+
|
119
|
+
def force_encoding new_encoding
|
120
|
+
new_encoding = Encoding.find new_encoding unless Encoding === new_encoding
|
121
|
+
@encoding = new_encoding
|
122
|
+
try_force_encoding @body
|
123
|
+
try_force_encoding @raw
|
124
|
+
@encoding
|
125
|
+
end
|
57
126
|
|
58
|
-
output.map do |line|
|
59
|
-
next unless line[0..2] == "-> "
|
60
|
-
resp << instance_eval(line[2..-1])
|
61
|
-
end
|
62
127
|
|
63
|
-
|
64
|
-
|
128
|
+
##
|
129
|
+
# If there was an error parsing the input as a standard http response,
|
130
|
+
# the input is assumed to be a body and HeadlessResponse is used.
|
131
|
+
|
132
|
+
def headless?
|
133
|
+
HeadlessResponse === @_res
|
134
|
+
end
|
135
|
+
|
65
136
|
|
137
|
+
##
|
138
|
+
# Ruby inspect.
|
139
|
+
|
140
|
+
def inspect
|
141
|
+
"#<#{self.class}:#{self.code} #{self['Content-Type']}>"
|
142
|
+
end
|
66
143
|
|
67
|
-
##
|
68
|
-
# Instantiates helper attributes from the debug socket io.
|
69
144
|
|
70
|
-
|
71
|
-
|
145
|
+
##
|
146
|
+
# Returns the body data parsed according to the content type.
|
147
|
+
# If no parser is given will look for the default parser based on
|
148
|
+
# the Content-Type, or will return the cached parsed body if available.
|
72
149
|
|
73
|
-
|
74
|
-
|
75
|
-
@body ||= @raw.split("\r\n\r\n",2)[1]
|
150
|
+
def parsed_body parser=nil
|
151
|
+
@parsed_body ||= nil
|
76
152
|
|
77
|
-
|
153
|
+
return @parsed_body if @parsed_body && !parser
|
78
154
|
|
79
|
-
|
80
|
-
|
81
|
-
self
|
155
|
+
if String === parser
|
156
|
+
parser = Kronk.parser_for(parser) || Kronk.find_const(parser)
|
82
157
|
end
|
83
158
|
|
159
|
+
parser ||= @parser
|
84
160
|
|
85
|
-
|
86
|
-
|
87
|
-
# "binary" if charset is unavailable.
|
88
|
-
# Returns "utf-8" if no content type header is missing.
|
161
|
+
raise MissingParser,
|
162
|
+
"No parser for Content-Type: #{@_res['Content-Type']}" unless parser
|
89
163
|
|
90
|
-
|
91
|
-
|
164
|
+
@parsed_body = parser.parse self.body
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
##
|
169
|
+
# Returns the parsed header hash.
|
92
170
|
|
93
|
-
|
171
|
+
def parsed_header include_headers=true
|
172
|
+
headers = @_res.to_hash.dup
|
94
173
|
|
95
|
-
|
96
|
-
|
174
|
+
case include_headers
|
175
|
+
when nil, false
|
176
|
+
nil
|
177
|
+
|
178
|
+
when Array, String
|
179
|
+
include_headers = [*include_headers].map{|h| h.to_s.downcase}
|
180
|
+
|
181
|
+
headers.each do |key, value|
|
182
|
+
headers.delete key unless
|
183
|
+
include_headers.include? key.to_s.downcase
|
97
184
|
end
|
98
185
|
|
99
|
-
|
186
|
+
headers
|
187
|
+
|
188
|
+
when true
|
189
|
+
headers
|
100
190
|
end
|
191
|
+
end
|
101
192
|
|
102
193
|
|
103
|
-
|
104
|
-
|
105
|
-
# it responds to 'force_encoding'.
|
106
|
-
# Returns the string given with the new encoding.
|
194
|
+
##
|
195
|
+
# Returns the header portion of the raw http response.
|
107
196
|
|
108
|
-
|
109
|
-
|
110
|
-
str
|
111
|
-
end
|
197
|
+
def raw_header include_headers=true
|
198
|
+
headers = "#{@raw.split("\r\n\r\n", 2)[0]}\r\n"
|
112
199
|
|
200
|
+
case include_headers
|
201
|
+
when nil, false
|
202
|
+
nil
|
113
203
|
|
114
|
-
|
115
|
-
|
204
|
+
when Array, String
|
205
|
+
includes = [*include_headers].join("|")
|
206
|
+
headers.scan(%r{^((?:#{includes}): [^\n]*\n)}im).flatten.join
|
116
207
|
|
117
|
-
|
118
|
-
|
208
|
+
when true
|
209
|
+
headers
|
119
210
|
end
|
211
|
+
end
|
120
212
|
|
121
213
|
|
122
|
-
|
123
|
-
|
124
|
-
# If no parser is given will look for the default parser based on
|
125
|
-
# the Content-Type, or will return the cached parsed body if available.
|
214
|
+
##
|
215
|
+
# Check if this is a redirect response.
|
126
216
|
|
127
|
-
|
128
|
-
|
217
|
+
def redirect?
|
218
|
+
@code.to_s =~ /^30\d$/
|
219
|
+
end
|
129
220
|
|
130
|
-
return @parsed_body if @parsed_body && !parser
|
131
221
|
|
132
|
-
|
133
|
-
|
134
|
-
|
222
|
+
##
|
223
|
+
# Follow the redirect and return a new Response instance.
|
224
|
+
# Returns nil if not redirect-able.
|
135
225
|
|
136
|
-
|
226
|
+
def follow_redirect opts={}
|
227
|
+
return if !redirect?
|
228
|
+
Request.new(@_res['Location'], opts).retrieve
|
229
|
+
end
|
137
230
|
|
138
|
-
raise MissingParser,
|
139
|
-
"No parser for Content-Type: #{self['Content-Type']}" unless parser
|
140
231
|
|
141
|
-
|
142
|
-
|
232
|
+
##
|
233
|
+
# Returns the raw response with selective headers and/or the body of
|
234
|
+
# the response. Supports the following options:
|
235
|
+
# :no_body:: Bool - Don't return the body; default nil
|
236
|
+
# :with_headers:: Bool/String/Array - Return headers; default nil
|
143
237
|
|
238
|
+
def selective_string options={}
|
239
|
+
str = @body unless options[:no_body]
|
144
240
|
|
145
|
-
|
146
|
-
|
241
|
+
if options[:with_headers]
|
242
|
+
header = raw_header(options[:with_headers])
|
243
|
+
str = [header, str].compact.join "\r\n"
|
244
|
+
end
|
245
|
+
|
246
|
+
str
|
247
|
+
end
|
147
248
|
|
148
|
-
def parsed_header include_headers=true
|
149
|
-
headers = self.to_hash.dup
|
150
249
|
|
151
|
-
|
152
|
-
|
153
|
-
|
250
|
+
##
|
251
|
+
# Returns the parsed response with selective headers and/or the body of
|
252
|
+
# the response. Supports the following options:
|
253
|
+
# :no_body:: Bool - Don't return the body; default nil
|
254
|
+
# :with_headers:: Bool/String/Array - Return headers; default nil
|
255
|
+
# :parser:: Object - The parser to use for the body; default nil
|
256
|
+
# :ignore_data:: String/Array - Removes the data from given data paths
|
257
|
+
# :only_data:: String/Array - Extracts the data from given data paths
|
258
|
+
|
259
|
+
def selective_data options={}
|
260
|
+
data = nil
|
261
|
+
|
262
|
+
unless options[:no_body]
|
263
|
+
data = parsed_body options[:parser]
|
264
|
+
end
|
154
265
|
|
155
|
-
|
156
|
-
|
266
|
+
if options[:with_headers]
|
267
|
+
data = [parsed_header(options[:with_headers]), data].compact
|
268
|
+
end
|
157
269
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
270
|
+
Path::Transaction.run data, options do |t|
|
271
|
+
t.select(*options[:only_data])
|
272
|
+
t.delete(*options[:ignore_data])
|
273
|
+
end
|
274
|
+
end
|
162
275
|
|
163
|
-
headers
|
164
276
|
|
165
|
-
|
166
|
-
|
167
|
-
|
277
|
+
##
|
278
|
+
# Returns a String representation of the response, the response body,
|
279
|
+
# or the response headers, parsed or in raw format.
|
280
|
+
# Options supported are:
|
281
|
+
# :parser:: Object/String - the parser for the body; default nil (raw)
|
282
|
+
# :struct:: Boolean - Return data types instead of values
|
283
|
+
# :only_data:: String/Array - extracts the data from given data paths
|
284
|
+
# :ignore_data:: String/Array - defines which data points to exclude
|
285
|
+
# :raw:: Boolean - Force using the unparsed raw response
|
286
|
+
# :keep_indicies:: Boolean - indicies of modified arrays display as hashes.
|
287
|
+
# :with_headers:: Boolean/String/Array - defines which headers to include
|
288
|
+
|
289
|
+
def stringify options={}
|
290
|
+
if !options[:raw] && (options[:parser] || @parser)
|
291
|
+
data = selective_data options
|
292
|
+
Diff.ordered_data_string data, options[:struct]
|
293
|
+
else
|
294
|
+
selective_string options
|
168
295
|
end
|
169
296
|
|
297
|
+
rescue MissingParser
|
298
|
+
Cmd.verbose "Warning: No parser for #{@_res['Content-Type']} [#{@uri}]"
|
299
|
+
selective_string options
|
300
|
+
end
|
170
301
|
|
171
|
-
##
|
172
|
-
# Returns the header portion of the raw http response.
|
173
302
|
|
174
|
-
|
175
|
-
|
303
|
+
##
|
304
|
+
# Check if this is a 2XX response.
|
176
305
|
|
177
|
-
|
178
|
-
|
179
|
-
|
306
|
+
def success?
|
307
|
+
@code.to_s =~ /^2\d\d$/
|
308
|
+
end
|
180
309
|
|
181
|
-
when Array, String
|
182
|
-
includes = [*include_headers].join("|")
|
183
|
-
headers.scan(%r{^((?:#{includes}): [^\n]*\n)}im).flatten.join
|
184
310
|
|
185
|
-
|
186
|
-
|
187
|
-
|
311
|
+
##
|
312
|
+
# Assign how long the request took in seconds.
|
313
|
+
|
314
|
+
def time= new_time
|
315
|
+
@time = new_time
|
316
|
+
@byterate = self.total_bytes / @time.to_f if @raw && @time > 0
|
317
|
+
@time
|
318
|
+
end
|
319
|
+
|
320
|
+
|
321
|
+
##
|
322
|
+
# Number of bytes of the response including the header.
|
323
|
+
|
324
|
+
def total_bytes
|
325
|
+
self.raw.bytes.count
|
326
|
+
end
|
327
|
+
|
328
|
+
|
329
|
+
private
|
330
|
+
|
331
|
+
|
332
|
+
##
|
333
|
+
# Creates a Net::HTTPRequest instance from an IO instance.
|
334
|
+
|
335
|
+
def request_from_io resp_io
|
336
|
+
# On windows, read the full file and insert contents into
|
337
|
+
# a StringIO to avoid failures with IO#read_nonblock
|
338
|
+
if Kronk::Cmd.windows? && File === resp_io
|
339
|
+
resp_io = WinFileIO.new resp_io.path, io.read
|
188
340
|
end
|
189
341
|
|
342
|
+
io = Net::BufferedIO === resp_io ? resp_io : Net::BufferedIO.new(resp_io)
|
343
|
+
io.debug_output = debug_io = StringIO.new
|
344
|
+
|
345
|
+
begin
|
346
|
+
resp = Net::HTTPResponse.read_new io
|
347
|
+
resp.reading_body io, true do;end
|
190
348
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
# :no_body:: Bool - Don't return the body; default nil
|
195
|
-
# :with_headers:: Bool/String/Array - Return headers; default nil
|
349
|
+
rescue Net::HTTPBadResponse
|
350
|
+
ext = "text/html"
|
351
|
+
ext = File.extname(resp_io.path) if WinFileIO === resp_io
|
196
352
|
|
197
|
-
|
198
|
-
|
353
|
+
resp_io.rewind
|
354
|
+
resp = HeadlessResponse.new resp_io.read, ext
|
199
355
|
|
200
|
-
|
201
|
-
|
202
|
-
|
356
|
+
rescue EOFError
|
357
|
+
# If no response was read because it's too short
|
358
|
+
unless resp
|
359
|
+
resp_io.rewind
|
360
|
+
resp = HeadlessResponse.new resp_io.read, "html"
|
203
361
|
end
|
362
|
+
end
|
204
363
|
|
205
|
-
|
364
|
+
resp.instance_eval do
|
365
|
+
@socket ||= true
|
366
|
+
@read ||= true
|
206
367
|
end
|
207
368
|
|
369
|
+
[resp, debug_io]
|
370
|
+
end
|
371
|
+
|
208
372
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
# :no_body:: Bool - Don't return the body; default nil
|
213
|
-
# :with_headers:: Bool/String/Array - Return headers; default nil
|
214
|
-
# :parser:: Object - The parser to use for the body; default nil
|
215
|
-
# :ignore_data:: String/Array - Removes the data from given data paths
|
216
|
-
# :only_data:: String/Array - Extracts the data from given data paths
|
373
|
+
##
|
374
|
+
# Read the raw response from a debug_output instance and return an array
|
375
|
+
# containing the raw request, response, and number of bytes received.
|
217
376
|
|
218
|
-
|
219
|
-
|
377
|
+
def read_raw_from debug_io
|
378
|
+
req = nil
|
379
|
+
resp = ""
|
380
|
+
bytes = nil
|
220
381
|
|
221
|
-
|
222
|
-
|
223
|
-
end
|
382
|
+
debug_io.rewind
|
383
|
+
output = debug_io.read.split "\n"
|
224
384
|
|
225
|
-
|
226
|
-
|
227
|
-
|
385
|
+
if output.first =~ %r{<-\s(.*)}
|
386
|
+
req = instance_eval $1
|
387
|
+
output.delete_at 0
|
388
|
+
end
|
389
|
+
|
390
|
+
if output.last =~ %r{read (\d+) bytes}
|
391
|
+
bytes = $1.to_i
|
392
|
+
output.delete_at(-1)
|
393
|
+
end
|
228
394
|
|
229
|
-
|
395
|
+
output.map do |line|
|
396
|
+
next unless line[0..2] == "-> "
|
397
|
+
resp << instance_eval(line[2..-1])
|
230
398
|
end
|
399
|
+
|
400
|
+
[req, resp, bytes]
|
401
|
+
end
|
402
|
+
|
403
|
+
|
404
|
+
##
|
405
|
+
# Assigns self.encoding to the passed string if
|
406
|
+
# it responds to 'force_encoding'.
|
407
|
+
# Returns the string given with the new encoding.
|
408
|
+
|
409
|
+
def try_force_encoding str
|
410
|
+
str.force_encoding @encoding if str.respond_to? :force_encoding
|
411
|
+
str
|
231
412
|
end
|
232
413
|
end
|
233
414
|
|
234
415
|
|
416
|
+
|
235
417
|
##
|
236
418
|
# Mock response object without a header for body-only http responses.
|
237
419
|
|
238
420
|
class HeadlessResponse
|
239
421
|
|
240
|
-
include Response::Helpers
|
241
|
-
|
242
422
|
attr_accessor :body, :code
|
243
423
|
|
244
424
|
def initialize body, file_ext=nil
|
245
425
|
@body = body
|
246
426
|
@raw = body
|
427
|
+
@code = "200"
|
247
428
|
|
248
|
-
encoding = body.encoding
|
429
|
+
encoding = body.respond_to?(:encoding) ? body.encoding : "UTF-8"
|
249
430
|
|
250
431
|
@header = {
|
251
|
-
'Content-Type' => ["
|
432
|
+
'Content-Type' => ["text/#{file_ext}; charset=#{encoding}"]
|
252
433
|
}
|
253
434
|
end
|
254
435
|
|
@@ -269,7 +450,7 @@ class Kronk
|
|
269
450
|
# Interface method only. Returns empty hash.
|
270
451
|
|
271
452
|
def to_hash
|
272
|
-
|
453
|
+
@header
|
273
454
|
end
|
274
455
|
end
|
275
456
|
end
|