kronk 1.8.7 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +22 -0
- data/Manifest.txt +6 -1
- data/README.rdoc +7 -2
- data/Rakefile +5 -4
- data/TODO.rdoc +3 -5
- data/lib/kronk.rb +20 -14
- data/lib/kronk/buffered_io.rb +7 -0
- data/lib/kronk/cmd.rb +25 -9
- data/lib/kronk/constants.rb +8 -0
- data/lib/kronk/http.rb +129 -2
- data/lib/kronk/multipart.rb +82 -0
- data/lib/kronk/multipart_io.rb +88 -0
- data/lib/kronk/player.rb +1 -1
- data/lib/kronk/player/benchmark.rb +69 -24
- data/lib/kronk/player/download.rb +87 -0
- data/lib/kronk/player/suite.rb +23 -11
- data/lib/kronk/request.rb +144 -77
- data/lib/kronk/response.rb +55 -18
- data/test/mocks/200_response.plist +1 -1
- data/test/test_cmd.rb +5 -3
- data/test/test_helper.rb +20 -10
- data/test/test_multipart.rb +144 -0
- data/test/test_multipart_io.rb +92 -0
- data/test/test_player.rb +7 -2
- data/test/test_request.rb +160 -43
- data/test/test_response.rb +34 -2
- metadata +27 -4
data/lib/kronk/response.rb
CHANGED
@@ -10,6 +10,7 @@ class Kronk
|
|
10
10
|
|
11
11
|
|
12
12
|
ENCODING_MATCHER = /(^|;\s?)charset=(.*?)\s*(;|$)/
|
13
|
+
DEFAULT_ENCODING = "ASCII-8BIT"
|
13
14
|
|
14
15
|
##
|
15
16
|
# Read http response from a file and return a Kronk::Response instance.
|
@@ -25,7 +26,7 @@ class Kronk
|
|
25
26
|
|
26
27
|
|
27
28
|
attr_reader :code, :io, :cookies, :headers
|
28
|
-
attr_accessor :read, :request, :stringify_opts, :time
|
29
|
+
attr_accessor :read, :request, :stringify_opts, :time, :conn_time
|
29
30
|
|
30
31
|
##
|
31
32
|
# Create a new Response object from a String or IO.
|
@@ -129,6 +130,7 @@ class Kronk
|
|
129
130
|
|
130
131
|
begin
|
131
132
|
read_body do |chunk|
|
133
|
+
chunk = chunk.dup
|
132
134
|
chunk = unzip chunk if gzip?
|
133
135
|
|
134
136
|
try_force_encoding chunk
|
@@ -169,10 +171,10 @@ class Kronk
|
|
169
171
|
|
170
172
|
|
171
173
|
##
|
172
|
-
#
|
174
|
+
# Current size of the http body in bytes.
|
173
175
|
|
174
176
|
def bytes
|
175
|
-
|
177
|
+
self.raw_body.bytes.count
|
176
178
|
end
|
177
179
|
|
178
180
|
|
@@ -232,6 +234,29 @@ class Kronk
|
|
232
234
|
end
|
233
235
|
|
234
236
|
|
237
|
+
##
|
238
|
+
# The extension or file type corresponding to the body,
|
239
|
+
# based on the Content-Type. Defaults to 'txt' if none can be determined
|
240
|
+
# or Content-Type is text/plain.
|
241
|
+
# application/json => 'json'
|
242
|
+
# text/html => 'html'
|
243
|
+
# application/foo+xml => 'xml'
|
244
|
+
|
245
|
+
def ext
|
246
|
+
file_ext =
|
247
|
+
if headers['content-type']
|
248
|
+
types = MIME::Types[headers['content-type'].sub(%r{/\w+\+}, '/')]
|
249
|
+
types[0].extensions[0] unless types.empty?
|
250
|
+
|
251
|
+
elsif uri
|
252
|
+
File.extname(uri.path)[1..-1]
|
253
|
+
end
|
254
|
+
|
255
|
+
file_ext = "txt" if !file_ext || file_ext.strip.empty?
|
256
|
+
file_ext
|
257
|
+
end
|
258
|
+
|
259
|
+
|
235
260
|
##
|
236
261
|
# Cookie header accessor.
|
237
262
|
|
@@ -264,10 +289,9 @@ class Kronk
|
|
264
289
|
|
265
290
|
def encoding
|
266
291
|
return @encoding if @encoding
|
267
|
-
|
268
|
-
c_type = headers["content-type"] =~ ENCODING_MATCHER
|
292
|
+
c_type = headers["content-type"].to_s =~ ENCODING_MATCHER
|
269
293
|
@encoding = $2 if c_type
|
270
|
-
@encoding ||=
|
294
|
+
@encoding ||= DEFAULT_ENCODING
|
271
295
|
@encoding = Encoding.find(@encoding) if defined?(Encoding)
|
272
296
|
@encoding
|
273
297
|
end
|
@@ -309,8 +333,9 @@ class Kronk
|
|
309
333
|
# Ruby inspect.
|
310
334
|
|
311
335
|
def inspect
|
312
|
-
content_type = headers['content-type'] || "text/
|
313
|
-
"#<#{self.class}:#{@code} #{content_type}
|
336
|
+
content_type = headers['content-type'] || "text/plain"
|
337
|
+
"#<#{self.class}:#{@code} #{content_type} \
|
338
|
+
#{total_bytes}/#{expected_bytes}bytes>"
|
314
339
|
end
|
315
340
|
|
316
341
|
|
@@ -329,8 +354,8 @@ class Kronk
|
|
329
354
|
# Check if connection should stay alive.
|
330
355
|
|
331
356
|
def keep_alive?
|
332
|
-
@headers['connection'].to_s.include?('
|
333
|
-
@headers['proxy-connection'].to_s.include?('
|
357
|
+
@headers['connection'].to_s.include?('Keep-Alive') ||
|
358
|
+
@headers['proxy-connection'].to_s.include?('Keep-Alive')
|
334
359
|
end
|
335
360
|
|
336
361
|
alias connection_keep_alive? keep_alive?
|
@@ -406,7 +431,7 @@ class Kronk
|
|
406
431
|
# The parser to use on the body.
|
407
432
|
|
408
433
|
def parser
|
409
|
-
@parser ||= Kronk.parser_for
|
434
|
+
@parser ||= Kronk.parser_for self.ext
|
410
435
|
end
|
411
436
|
|
412
437
|
|
@@ -434,7 +459,7 @@ class Kronk
|
|
434
459
|
# Returns the body portion of the raw http response.
|
435
460
|
|
436
461
|
def raw_body
|
437
|
-
headless? ? raw :
|
462
|
+
headless? ? @raw.to_s : @body.to_s
|
438
463
|
end
|
439
464
|
|
440
465
|
|
@@ -554,12 +579,13 @@ class Kronk
|
|
554
579
|
# :only_data:: String/Array - Extracts the data from given data paths
|
555
580
|
#
|
556
581
|
# Example:
|
557
|
-
# response.data :transform => [:delete, ["foo/0", "bar/1"]]
|
582
|
+
# response.data :transform => [[:delete, ["foo/0", "bar/1"]]]
|
558
583
|
# response.data do |trans|
|
559
584
|
# trans.delete "foo/0", "bar/1"
|
560
585
|
# end
|
561
586
|
#
|
562
|
-
# See Path::Transaction for supported transform actions
|
587
|
+
# See Path::Transaction for supported transform actions in the
|
588
|
+
# {ruby-path gem}[http://github.com/yaksnrainbows/ruby-path].
|
563
589
|
|
564
590
|
def data opts={}
|
565
591
|
data = nil
|
@@ -672,6 +698,16 @@ class Kronk
|
|
672
698
|
# Number of bytes of the response including the header.
|
673
699
|
|
674
700
|
def total_bytes
|
701
|
+
return raw.bytes.count if @read
|
702
|
+
return raw_header.bytes.count unless body_permitted?
|
703
|
+
raw_header.to_s.bytes.count + bytes + 2
|
704
|
+
end
|
705
|
+
|
706
|
+
|
707
|
+
##
|
708
|
+
# Expected number of bytes to read from the server, including the header.
|
709
|
+
|
710
|
+
def expected_bytes
|
675
711
|
return raw.bytes.count if @read
|
676
712
|
return raw_header.bytes.count unless body_permitted?
|
677
713
|
raw_header.to_s.bytes.count + (content_length || range_length).to_i + 2
|
@@ -738,13 +774,14 @@ class Kronk
|
|
738
774
|
|
739
775
|
buff_io.rewind
|
740
776
|
|
741
|
-
|
742
|
-
|
777
|
+
ctype = ["text/plain"]
|
778
|
+
ctype = MIME::Types.of(buff_io.io.path).concat ctype if
|
779
|
+
File === buff_io.io
|
743
780
|
|
744
781
|
encoding = buff_io.io.respond_to?(:external_encoding) ?
|
745
|
-
buff_io.io.external_encoding :
|
782
|
+
buff_io.io.external_encoding : DEFAULT_ENCODING
|
746
783
|
@headers = {
|
747
|
-
'content-type' => "
|
784
|
+
'content-type' => "#{ctype[0]}; charset=#{encoding}",
|
748
785
|
}
|
749
786
|
|
750
787
|
@headless = true
|
data/test/test_cmd.rb
CHANGED
@@ -321,11 +321,11 @@ class TestCmd < Test::Unit::TestCase
|
|
321
321
|
opts = Kronk::Cmd.parse_args %w{uri -p mock_file}
|
322
322
|
assert_equal mock_file, opts[:player].input.io
|
323
323
|
|
324
|
-
opts = Kronk::Cmd.parse_args %w{uri --benchmark mock_file}
|
324
|
+
opts = Kronk::Cmd.parse_args %w{uri --benchmark -p mock_file}
|
325
325
|
assert_equal mock_file, opts[:player].input.io
|
326
326
|
assert_equal Kronk::Player::Benchmark, opts[:player].class
|
327
327
|
|
328
|
-
opts = Kronk::Cmd.parse_args %w{uri --stream mock_file}
|
328
|
+
opts = Kronk::Cmd.parse_args %w{uri --stream -p mock_file}
|
329
329
|
assert_equal mock_file, opts[:player].input.io
|
330
330
|
assert_equal Kronk::Player::Stream, opts[:player].class
|
331
331
|
end
|
@@ -366,7 +366,8 @@ class TestCmd < Test::Unit::TestCase
|
|
366
366
|
|
367
367
|
def test_parse_args_http_options
|
368
368
|
opts = Kronk::Cmd.parse_args %w{uri -A foo -L --no-cookies -? bar
|
369
|
-
--suff /tail -X PUT -x example.com:2000 --form foo=bar
|
369
|
+
--suff /tail -X PUT -x example.com:2000 --form foo=bar
|
370
|
+
--form-upload file=test/mocks/200_response.json}
|
370
371
|
|
371
372
|
assert_equal "foo", opts[:user_agent]
|
372
373
|
assert_equal true, opts[:follow_redirects]
|
@@ -375,6 +376,7 @@ class TestCmd < Test::Unit::TestCase
|
|
375
376
|
assert_equal "/tail", opts[:uri_suffix]
|
376
377
|
assert_equal "PUT", opts[:http_method]
|
377
378
|
assert_equal "foo=bar", opts[:form]
|
379
|
+
assert_equal "file=test/mocks/200_response.json", opts[:form_upload]
|
378
380
|
assert_equal({:host => "example.com", :port => "2000"}, opts[:proxy])
|
379
381
|
|
380
382
|
opts = Kronk::Cmd.parse_args %w{uri -L 3}
|
data/test/test_helper.rb
CHANGED
@@ -160,33 +160,43 @@ ensure
|
|
160
160
|
end
|
161
161
|
|
162
162
|
|
163
|
-
def expect_request req_method, url,
|
163
|
+
def expect_request req_method, url, opts={}
|
164
164
|
uri = URI.parse url
|
165
165
|
|
166
|
-
resp = Kronk::Response.new(
|
167
|
-
resp.stubs(:code).returns(
|
166
|
+
resp = Kronk::Response.new(opts[:returns] || mock_200_response)
|
167
|
+
resp.stubs(:code).returns(opts[:status] || '200')
|
168
168
|
resp.stubs(:to_hash).returns Hash.new
|
169
169
|
|
170
170
|
http = mock 'http'
|
171
171
|
req = mock 'req'
|
172
172
|
|
173
|
-
data =
|
173
|
+
data = opts[:data]
|
174
174
|
data &&= Hash === data ? Kronk::Request.build_query(data) : data.to_s
|
175
175
|
|
176
|
-
|
176
|
+
req.stubs(:body).returns(data)
|
177
|
+
req.stubs(:body_stream).returns(StringIO.new(data.to_s))
|
178
|
+
|
179
|
+
req.stubs(:[]).with('Content-Length').returns(data)
|
180
|
+
req.stubs(:[]=)
|
181
|
+
|
182
|
+
headers = opts[:headers] || Hash.new
|
177
183
|
headers['User-Agent'] ||= Kronk::DEFAULT_USER_AGENT
|
184
|
+
headers['Connection'] ||= 'Keep-Alive'
|
178
185
|
|
179
|
-
|
186
|
+
http.expects(:started?).returns false
|
187
|
+
http.expects(:start)
|
180
188
|
req.expects(:body=).with(data)
|
181
189
|
|
182
190
|
Kronk::Request::VanillaRequest.expects(:new).
|
183
191
|
with(req_method.to_s.upcase, uri.request_uri, headers).returns req
|
184
192
|
|
185
|
-
|
193
|
+
proxy = opts[:proxy] || {}
|
194
|
+
|
195
|
+
Kronk::HTTP.expects(:new).
|
196
|
+
with(uri.host, uri.port, {:proxy => proxy, :ssl => !!opts[:ssl]}).
|
197
|
+
returns http
|
186
198
|
|
187
|
-
http.expects(:request).
|
188
|
-
with(req, nil, has_entry(:request => instance_of(Kronk::Request))).
|
189
|
-
returns resp
|
199
|
+
http.expects(:request).with(req, nil, {}).returns resp
|
190
200
|
|
191
201
|
yield http, req, resp if block_given?
|
192
202
|
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
|
3
|
+
class TestMultipart < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@multi = Kronk::Multipart.new "foobar"
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
def test_initialize
|
11
|
+
assert_equal "foobar", @multi.boundary
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def test_add
|
16
|
+
@multi.add "foo", "bar"
|
17
|
+
expected = [{'content-disposition' => 'form-data; name="foo"'}, "bar"]
|
18
|
+
assert_equal expected, @multi.parts.last
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def test_add_headers
|
23
|
+
@multi.add "foo", "bar", "X-Header" => "blah"
|
24
|
+
expected = [{'content-disposition' => 'form-data; name="foo"',
|
25
|
+
"X-Header" => "blah"}, "bar"]
|
26
|
+
assert_equal expected, @multi.parts.last
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def test_add_file
|
31
|
+
file = File.open("test/mocks/200_response.json", "rb")
|
32
|
+
@multi.add "foo", file
|
33
|
+
|
34
|
+
expected = [{
|
35
|
+
"content-disposition" =>
|
36
|
+
'form-data; name="foo"; filename="200_response.json"',
|
37
|
+
"Content-Type" => "application/json",
|
38
|
+
"Content-Transfer-Encoding" => "binary"
|
39
|
+
}, file]
|
40
|
+
|
41
|
+
assert_equal expected, @multi.parts.last
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def test_add_io
|
46
|
+
prd, pwr = IO.pipe
|
47
|
+
@multi.add "foo", prd
|
48
|
+
|
49
|
+
expected = [{
|
50
|
+
"content-disposition" => 'form-data; name="foo"',
|
51
|
+
"Content-Type" => "application/octet-stream",
|
52
|
+
"Content-Transfer-Encoding" => "binary"
|
53
|
+
}, prd]
|
54
|
+
|
55
|
+
assert_equal expected, @multi.parts.last
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def test_add_escaped_name
|
60
|
+
@multi.add "foo:10", "bar"
|
61
|
+
expected = [{'content-disposition' => 'form-data; name="foo:10"'}, "bar"]
|
62
|
+
assert_equal expected, @multi.parts.last
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def test_to_io
|
67
|
+
@multi.add "key1", "bar"
|
68
|
+
@multi.add "key2", "some value"
|
69
|
+
@multi.add "key3", "other thing"
|
70
|
+
|
71
|
+
io = @multi.to_io
|
72
|
+
assert_equal Kronk::MultipartIO, io.class
|
73
|
+
assert_equal 1, io.parts.length
|
74
|
+
assert_equal StringIO, io.parts.first.class
|
75
|
+
|
76
|
+
expected = <<-STR
|
77
|
+
--foobar\r
|
78
|
+
content-disposition: form-data; name="key1"\r
|
79
|
+
\r
|
80
|
+
bar\r
|
81
|
+
--foobar\r
|
82
|
+
content-disposition: form-data; name="key2"\r
|
83
|
+
\r
|
84
|
+
some value\r
|
85
|
+
--foobar\r
|
86
|
+
content-disposition: form-data; name="key3"\r
|
87
|
+
\r
|
88
|
+
other thing\r
|
89
|
+
--foobar--
|
90
|
+
STR
|
91
|
+
assert_equal expected.strip, io.read
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def test_to_io_with_file
|
96
|
+
file = File.open("test/mocks/200_response.json", "rb")
|
97
|
+
@multi.add "key1", "bar"
|
98
|
+
@multi.add "key2", "some value"
|
99
|
+
@multi.add "my_file", file
|
100
|
+
|
101
|
+
io = @multi.to_io
|
102
|
+
assert_equal 3, io.parts.length
|
103
|
+
assert_equal StringIO, io.parts.first.class
|
104
|
+
assert_equal file, io.parts[1]
|
105
|
+
assert_equal StringIO, io.parts.last.class
|
106
|
+
|
107
|
+
file_content = File.read("test/mocks/200_response.json")
|
108
|
+
|
109
|
+
head = <<-STR
|
110
|
+
--foobar\r
|
111
|
+
content-disposition: form-data; name="key1"\r
|
112
|
+
\r
|
113
|
+
bar\r
|
114
|
+
--foobar\r
|
115
|
+
content-disposition: form-data; name="key2"\r
|
116
|
+
\r
|
117
|
+
some value\r
|
118
|
+
--foobar\r
|
119
|
+
content-disposition: form-data; name="my_file"; filename="200_response.json"\r
|
120
|
+
STR
|
121
|
+
|
122
|
+
ctype_line = "Content-Type: application/json\r\n"
|
123
|
+
ctran_line = "Content-Transfer-Encoding: binary\r\n"
|
124
|
+
tail = "\r\n#{file_content}\r\n--foobar--"
|
125
|
+
|
126
|
+
output = io.read head.bytes.count
|
127
|
+
assert_equal head, output
|
128
|
+
|
129
|
+
output = ""
|
130
|
+
output << io.read(1) while output[-1..-1] != "\n"
|
131
|
+
|
132
|
+
comp1, comp2 = output.length == ctype_line.length ?
|
133
|
+
[ctype_line, ctran_line] : [ctran_line, ctype_line]
|
134
|
+
|
135
|
+
assert_equal comp1, output
|
136
|
+
|
137
|
+
output = ""
|
138
|
+
output << io.read(1) while output[-1..-1] != "\n"
|
139
|
+
|
140
|
+
assert_equal comp2, output
|
141
|
+
|
142
|
+
assert_equal tail, io.read
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
|
3
|
+
class TestMultipartIo < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
strio = StringIO.new "thing"
|
7
|
+
@io = Kronk::MultipartIO.new "foo", "bar", strio
|
8
|
+
|
9
|
+
@pipe_rd, @pipe_wr = IO.pipe
|
10
|
+
@pipe_wr.write "thing"
|
11
|
+
@pipe_wr.close
|
12
|
+
@pio = Kronk::MultipartIO.new "foo", @pipe_rd, "bar"
|
13
|
+
|
14
|
+
@file = File.open 'test/mocks/200_response.json', "r"
|
15
|
+
@fio = Kronk::MultipartIO.new "foo", @file, "bar"
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def teardown
|
20
|
+
@file.close
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def test_initialize
|
25
|
+
assert_equal StringIO, @io.parts[0].class
|
26
|
+
assert_equal "foo", @io.parts[0].read
|
27
|
+
assert_equal StringIO, @io.parts[1].class
|
28
|
+
assert_equal "bar", @io.parts[1].read
|
29
|
+
assert_equal StringIO, @io.parts[2].class
|
30
|
+
assert_equal "thing", @io.parts[2].read
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def test_size
|
35
|
+
assert_equal 11, @io.size
|
36
|
+
assert_nil @pio.size
|
37
|
+
assert_equal((@file.size + 6), @fio.size)
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def test_read_all_strings
|
42
|
+
assert_equal "foobarthing", @io.read_all
|
43
|
+
assert_nil @io.read 1
|
44
|
+
assert_equal "", @io.read
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def test_read_all_file
|
49
|
+
assert_equal "foo#{File.read "test/mocks/200_response.json"}bar",
|
50
|
+
@fio.read_all
|
51
|
+
assert_nil @fio.read 1
|
52
|
+
assert_equal "", @fio.read
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def test_read_all_io
|
57
|
+
assert_equal "foothingbar",
|
58
|
+
@pio.read_all
|
59
|
+
assert_nil @pio.read 1
|
60
|
+
assert_equal "", @pio.read
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def test_read_all_after_read_partial
|
65
|
+
assert_equal "foo", @pio.read(3)
|
66
|
+
assert_equal "thingbar", @pio.read
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def test_eof
|
71
|
+
assert !@pio.eof?, "EOF should NOT be reached"
|
72
|
+
@pio.read 5
|
73
|
+
|
74
|
+
assert !@pio.eof?, "EOF should NOT be reached"
|
75
|
+
@pio.read_all
|
76
|
+
|
77
|
+
assert @pio.eof?, "EOF should be reached"
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
def test_close
|
82
|
+
@pio.parts.each do |io|
|
83
|
+
assert !io.closed?, "#{io.inspect} should NOT be closed"
|
84
|
+
end
|
85
|
+
|
86
|
+
@pio.close
|
87
|
+
|
88
|
+
@pio.parts.each do |io|
|
89
|
+
assert io.closed?, "#{io.inspect} should be closed"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|