kronk 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|