1 # -*- encoding: binary -*-
2 $stderr.sync = $stdout.sync = true
11 # used to test subclasses
12 class FooString < String
15 class TestClogger < Test::Unit::TestCase
16 include Clogger::Format
20 "REQUEST_METHOD" => "GET",
21 "HTTP_VERSION" => "HTTP/1.0",
22 "HTTP_USER_AGENT" => 'echo and socat \o/',
23 "PATH_INFO" => "/hello",
24 "QUERY_STRING" => "goodbye=true",
25 "rack.errors" => $stderr,
26 "rack.input" => File.open('/dev/null', 'rb'),
27 "REMOTE_ADDR" => 'home',
32 Clogger.new(lambda { |env| [ 0, {}, [] ] })
36 assert_raise(ArgumentError) { Clogger.new }
40 cl = Clogger.new(lambda { |env| [ 0, {}, [] ] }, :logger => $stderr)
41 assert_kind_of(Integer, cl.fileno)
42 assert_equal $stderr.fileno, cl.fileno
45 def test_init_stringio
46 cl = Clogger.new(lambda { |env| [ 0, {}, [] ] }, :logger => StringIO.new)
50 def test_write_stringio
51 start = DateTime.now - 1
53 cl = Clogger.new(lambda { |env| [ "302 Found", {}, [] ] }, :logger => str)
54 status, headers, body = cl.call(@req)
55 assert_equal("302 Found", status)
56 assert_equal({}, headers)
57 body.each { |part| assert false }
60 r = %r{\Ahome - - \[[^\]]+\] "GET /hello\?goodbye=true HTTP/1.0" 302 -\n\z}
62 %r{\[([^\]]+)\]} =~ str
64 assert_nothing_raised {
65 tmp = DateTime.strptime($1, "%d/%b/%Y:%H:%M:%S %z")
68 assert tmp <= DateTime.now
71 def test_clen_stringio
72 start = DateTime.now - 1
74 app = lambda { |env| [ 301, {'Content-Length' => '5'}, ['abcde'] ] }
76 assert format.gsub!(/response_length/, 'sent_http_content_length')
77 cl = Clogger.new(app, :logger => str, :format => format)
78 status, headers, body = cl.call(@req)
79 assert_equal(301, status)
80 assert_equal({'Content-Length' => '5'}, headers)
81 body.each { |part| assert_equal('abcde', part) }
83 r = %r{\Ahome - - \[[^\]]+\] "GET /hello\?goodbye=true HTTP/1.0" 301 5\n\z}
85 %r{\[([^\]]+)\]} =~ str
87 assert_nothing_raised {
88 tmp = DateTime.strptime($1, "%d/%b/%Y:%H:%M:%S %z")
91 assert tmp <= DateTime.now
94 def test_compile_ambiguous
95 cl = Clogger.new(nil, :logger => $stderr)
99 '$remote_addr $$$$pid' \
103 [ Clogger::OP_REQUEST, "REMOTE_ADDR" ],
104 [ Clogger::OP_LITERAL, " " ],
105 [ Clogger::OP_LITERAL, "$$$" ],
106 [ Clogger::OP_SPECIAL, Clogger::SPECIAL_VARS[:pid] ],
107 [ Clogger::OP_LITERAL, "\n" ],
109 assert_equal expect, ary
112 def test_compile_auto_newline
113 cl = Clogger.new(nil, :logger => $stderr)
115 cl.instance_eval { ary = compile_format('$remote_addr $request') }
117 [ Clogger::OP_REQUEST, "REMOTE_ADDR" ],
118 [ Clogger::OP_LITERAL, " " ],
119 [ Clogger::OP_SPECIAL, Clogger::SPECIAL_VARS[:request] ],
120 [ Clogger::OP_LITERAL, "\n" ],
122 assert_equal expect, ary
127 fmt = '$remote_addr $pid $remote_user [$time_local] ' \
128 '"$request" $status $body_bytes_sent "$http_referer" ' \
129 '"$http_user_agent" "$http_cookie" $request_time $http_host'
130 app = lambda { |env| [ 302, {}, [] ] }
131 cl = Clogger.new(app, :logger => str, :format => fmt)
132 cookie = "foo=bar#{'f' * 256}".freeze
134 'HTTP_HOST' => 'example.com:12345',
135 'HTTP_COOKIE' => cookie,
137 req = @req.merge(req)
138 body = cl.call(req).last
139 body.each { |part| part }
142 assert(str.size > 128)
143 assert_match %r["echo and socat \\o/" "#{cookie}" \d+\.\d{3}], str
144 assert_match %r["#{cookie}" \d+\.\d{3} example\.com:12345\n\z], str
148 cl = Clogger.new(nil, :logger => $stderr)
151 ary = compile_format(
152 '$remote_addr - $remote_user [$time_local] ' \
153 '"$request" $status $body_bytes_sent "$http_referer" ' \
154 '"$http_user_agent" "$http_cookie" $request_time ' \
155 '$env{rack.url_scheme}' \
159 [ Clogger::OP_REQUEST, "REMOTE_ADDR" ],
160 [ Clogger::OP_LITERAL, " - " ],
161 [ Clogger::OP_REQUEST, "REMOTE_USER" ],
162 [ Clogger::OP_LITERAL, " [" ],
163 [ Clogger::OP_TIME_LOCAL, '%d/%b/%Y:%H:%M:%S %z' ],
164 [ Clogger::OP_LITERAL, "] \"" ],
165 [ Clogger::OP_SPECIAL, Clogger::SPECIAL_VARS[:request] ],
166 [ Clogger::OP_LITERAL, "\" "],
167 [ Clogger::OP_SPECIAL, Clogger::SPECIAL_VARS[:status] ],
168 [ Clogger::OP_LITERAL, " "],
169 [ Clogger::OP_SPECIAL, Clogger::SPECIAL_VARS[:body_bytes_sent] ],
170 [ Clogger::OP_LITERAL, " \"" ],
171 [ Clogger::OP_REQUEST, "HTTP_REFERER" ],
172 [ Clogger::OP_LITERAL, "\" \"" ],
173 [ Clogger::OP_REQUEST, "HTTP_USER_AGENT" ],
174 [ Clogger::OP_LITERAL, "\" \"" ],
175 [ Clogger::OP_REQUEST, "HTTP_COOKIE" ],
176 [ Clogger::OP_LITERAL, "\" " ],
177 [ Clogger::OP_REQUEST_TIME, '%d.%03d', 1000 ],
178 [ Clogger::OP_LITERAL, " " ],
179 [ Clogger::OP_REQUEST, "rack.url_scheme" ],
180 [ Clogger::OP_LITERAL, "\n" ],
182 assert_equal expect, ary
186 current = Thread.current.to_s
188 app = lambda { |env| [ 302, {}, [] ] }
189 cl = Clogger.new(app,
191 :format => "-$e{Thread.current}-\n")
192 status, headers, body = cl.call(@req)
193 assert_equal "-#{current}-\n", str.string
198 app = lambda { |env| [ 302, {}, [] ] }
199 cl = Clogger.new(app, :logger => str, :format => "[$pid]\n")
200 status, headers, body = cl.call(@req)
201 assert_equal "[#$$]\n", str.string
206 app = lambda { |env| [ 302, {}, [] ] }
207 cl = Clogger.new(app, :logger => str, :format => "$ip")
208 req = @req.merge("HTTP_X_FORWARDED_FOR" => '192.168.1.1')
209 status, headers, body = cl.call(req)
210 assert_equal "192.168.1.1\n", str.string
213 status, headers, body = cl.call(@req)
214 assert_equal "home\n", str.string
220 start = DateTime.now - 1
222 app = lambda { |env| [ 200, {'Content-Length'=>'0'}, %w(a b c)] }
223 cl = Clogger.new(app, :logger => str, :format => Rack_1_0)
224 status, headers, body = cl.call(@req)
226 body.each { |s| tmp << s }
228 assert_equal %w(a b c), tmp
230 assert_match %r[" 200 3 \d+\.\d{4}\n\z], str
232 %r{\[(\d+/\w+/\d+ \d+:\d+:\d+)\]} =~ str
234 assert_nothing_raised { tmp = DateTime.strptime($1, "%d/%b/%Y %H:%M:%S") }
236 assert tmp <= DateTime.now
241 app = lambda { |env| [ 200, {}, [] ] }
242 cl = Clogger.new(app, :logger => str, :format => '$msec')
243 status, header, bodies = cl.call(@req)
244 assert_match %r(\A\d+\.\d{3}\n\z), str.string
249 app = lambda { |env| [ 200, {}, [] ] }
250 cl = Clogger.new(app, :logger => str, :format => '$usec')
251 status, header, bodies = cl.call(@req)
252 assert_match %r(\A\d+\.\d{6}\n\z), str.string
257 app = lambda { |env| [ 200, {}, [] ] }
258 cl = Clogger.new(app, :logger => str, :format => '$time{0}')
259 status, header, bodies = cl.call(@req)
260 assert_match %r(\A\d+\n\z), str.string
265 app = lambda { |env| [ 200, {}, [] ] }
266 cl = Clogger.new(app, :logger => str, :format => '$time{1}')
267 status, header, bodies = cl.call(@req)
268 assert_match %r(\A\d+\.\d\n\z), str.string
271 def test_request_length
273 input = StringIO.new('.....')
274 app = lambda { |env| [ 200, {}, [] ] }
275 cl = Clogger.new(app, :logger => str, :format => '$request_length')
276 status, header, bodies = cl.call(@req.merge('rack.input' => input))
277 assert_equal "5\n", str.string
280 def test_response_length_0
282 app = lambda { |env| [ 200, {}, [] ] }
283 cl = Clogger.new(app, :logger => str, :format => '$response_length')
284 status, header, bodies = cl.call(@req)
285 bodies.each { |part| part }
287 assert_equal "-\n", str.string
291 start = DateTime.now - 1
293 app = lambda { |env| [ 200, {'Content-Length'=>'3'}, %w(a b c)] }
294 cl = Clogger.new(app, :logger => str, :format => Combined)
295 status, headers, body = cl.call(@req)
297 body.each { |s| tmp << s }
299 assert_equal %w(a b c), tmp
301 assert_match %r[" 200 3 "-" "echo and socat \\o/"\n\z], str
303 %r{\[(\d+/\w+/\d+:\d+:\d+:\d+ .+)\]} =~ str
305 assert_nothing_raised {
306 tmp = DateTime.strptime($1, "%d/%b/%Y:%H:%M:%S %z")
309 assert tmp <= DateTime.now
312 def test_rack_errors_fallback
314 app = lambda { |env| [ 200, {'Content-Length'=>'3'}, %w(a b c)] }
315 cl = Clogger.new(app, :format => '$pid')
316 req = @req.merge('rack.errors' => err)
317 status, headers, body = cl.call(req)
318 assert_equal "#$$\n", err.string
322 s_body = StringIO.new(%w(a b c).join("\n"))
323 app = lambda { |env| [ 200, {'Content-Length'=>'5'}, s_body] }
324 cl = Clogger.new(app, :logger => [], :format => '$pid')
325 status, headers, body = cl.call(@req)
326 assert ! s_body.closed?
327 assert_nothing_raised { body.close }
328 assert s_body.closed?
333 app = lambda { |env| [ 200, {'Content-Length'=>'5'}, [] ] }
334 cl = Clogger.new(app,
336 :format => '$http_user_agent "$request"')
338 'HTTP_USER_AGENT' => '"asdf"',
339 'QUERY_STRING' => 'sdf=bar"',
340 'PATH_INFO' => '/"<>"',
342 status, headers, body = cl.call(@req.merge(bad))
343 expect = '\x22asdf\x22 "GET /\x22<>\x22?sdf=bar\x22 HTTP/1.0"' << "\n"
344 assert_equal expect, str.string
347 # rack allows repeated headers with "\n":
348 # { 'Set-Cookie' => "a\nb" } =>
351 def test_escape_header_newlines
353 app = lambda { |env| [302, { 'Set-Cookie' => "a\nb" }, [] ] }
354 cl = Clogger.new(app, :logger => str, :format => '$sent_http_set_cookie')
356 assert_equal "a\\x0Ab\n", str.string
359 def test_request_uri_fallback
361 app = lambda { |env| [ 200, {}, [] ] }
362 cl = Clogger.new(app, :logger => str, :format => '$request_uri')
363 status, headers, body = cl.call(@req)
364 assert_equal "/hello?goodbye=true\n", str.string
367 def test_request_uri_set
369 app = lambda { |env| [ 200, {}, [] ] }
370 cl = Clogger.new(app, :logger => str, :format => '$request_uri')
371 status, headers, body = cl.call(@req.merge("REQUEST_URI" => '/zzz'))
372 assert_equal "/zzz\n", str.string
378 req = Rack::Request.new(env).cookies
381 cl = Clogger.new(app,
382 :format => '$cookie_foo $cookie_quux',
384 req = @req.merge('HTTP_COOKIE' => "foo=bar;quux=h&m")
385 status, headers, body = cl.call(req)
386 assert_equal "bar h&m\n", str.string
389 def test_bogus_app_response
391 app = lambda { |env| 302 }
392 cl = Clogger.new(app, :logger => str)
393 assert_raise(TypeError) { cl.call(@req) }
395 e = Regexp.quote " \"GET /hello?goodbye=true HTTP/1.0\" 500 -"
396 assert_match %r{#{e}$}m, str
399 def test_broken_header_response
401 app = lambda { |env| [302, [ %w(a) ], []] }
402 cl = Clogger.new(app, :logger => str, :format => '$sent_http_set_cookie')
403 assert_nothing_raised { cl.call(@req) }
406 def test_subclass_hash
408 req = Rack::Utils::HeaderHash.new(@req)
409 app = lambda { |env| [302, [ %w(a) ], []] }
410 cl = Clogger.new(app, :logger => str, :format => Rack_1_0)
411 assert_nothing_raised { cl.call(req).last.each {}.close }
415 def test_subclassed_string_req
418 @req.each { |key,value|
419 req[FooString.new(key)] = value.kind_of?(String) ?
420 FooString.new(value) : value
422 app = lambda { |env| [302, [ %w(a) ], []] }
423 cl = Clogger.new(app, :logger => str, :format => Rack_1_0)
424 assert_nothing_raised { cl.call(req).last.each {}.close }
428 def test_subclassed_string_in_body
432 app = lambda { |env| [302, [ %w(a) ], [FooString.new(body)]] }
433 cl = Clogger.new(app, :logger => str, :format => '$body_bytes_sent')
434 assert_nothing_raised { cl.call(@req).last.each { |x| r = x }.close }
436 assert_equal body.size.to_s << "\n", str.string
438 assert r.object_id != body.object_id
441 def test_http_09_request
443 app = lambda { |env| [302, [ %w(a) ], []] }
444 cl = Clogger.new(app, :logger => str, :format => '$request')
446 req.delete 'HTTP_VERSION'
448 assert_equal "GET /hello?goodbye=true\n", str.string
451 def test_request_method_only
453 app = lambda { |env| [302, [ %w(a) ], []] }
454 cl = Clogger.new(app, :logger => str, :format => '$request_method')
456 assert_equal "GET\n", str.string
459 def test_content_length_null
461 app = lambda { |env| [302, [ %w(a) ], []] }
462 cl = Clogger.new(app, :logger => str, :format => '$content_length')
464 assert_equal "-\n", str.string
467 def test_content_length_set
469 app = lambda { |env| [302, [ %w(a) ], []] }
470 cl = Clogger.new(app, :logger => str, :format => '$content_length')
471 cl.call(@req.merge('CONTENT_LENGTH' => '5'))
472 assert_equal "5\n", str.string
475 def test_http_content_type_fallback
477 app = lambda { |env| [302, [ %w(a) ], []] }
478 cl = Clogger.new(app, :logger => str, :format => '$http_content_type')
479 cl.call(@req.merge('CONTENT_TYPE' => 'text/plain'))
480 assert_equal "text/plain\n", str.string
483 def test_clogger_synced
485 logger = Struct.new(:sync, :io).new(false, io)
490 app = lambda { |env| [302, [ %w(a) ], []] }
491 cl = Clogger.new(app, :logger => logger)
495 def test_clogger_unsyncable
497 assert ! logger.respond_to?('sync=')
498 app = lambda { |env| [302, [ %w(a) ], []] }
499 assert_nothing_raised { Clogger.new(app, :logger => logger) }
502 def test_clogger_no_ORS
504 app = lambda { |env| [302, [ %w(a) ], []] }
505 cl = Clogger.new(app, :logger => s, :format => "$request", :ORS => "")
507 assert_equal "GET /hello?goodbye=true HTTP/1.0", s
510 def test_clogger_weird_ORS
512 app = lambda { |env| [302, [ %w(a) ], []] }
513 cl = Clogger.new(app, :logger => s, :format => "<$request", :ORS => ">")
515 assert_equal "<GET /hello?goodbye=true HTTP/1.0>", s
518 def test_clogger_body_not_closeable
520 app = lambda { |env| [302, [ %w(a) ], []] }
521 cl = Clogger.new(app, :logger => s)
522 status, headers, body = cl.call(@req)
523 assert_nil body.close
526 def test_clogger_response_frozen
527 response = [ 200, { "AAAA" => "AAAA"}.freeze, [].freeze ].freeze
529 app = Rack::Builder.new do
530 use Clogger, :logger => s, :format => "$request_time $http_host"
531 run lambda { |env| response }
533 assert_nothing_raised do
535 resp = app.call(@req)
536 assert ! resp.frozen?
537 resp.last.each { |x| }
542 def test_clogger_body_close_return_value
548 app = lambda { |env| [302, [ %w(a) ], body ] }
549 cl = Clogger.new(app, :logger => s)
550 status, headers, body = cl.call(@req)
551 assert_equal :foo, body.close
554 def test_clogger_auto_reentrant_true
557 app = lambda { |env| [302, [ %w(a) ], body ] }
558 cl = Clogger.new(app, :logger => s, :format => "$request_time")
559 @req['rack.multithread'] = true
560 status, headers, body = cl.call(@req)
564 def test_clogger_auto_reentrant_false
567 app = lambda { |env| [302, [ %w(a) ], body ] }
568 cl = Clogger.new(app, :logger => s, :format => "$request_time")
569 @req['rack.multithread'] = false
570 status, headers, body = cl.call(@req)
571 assert ! cl.reentrant?
574 def test_clogger_auto_reentrant_forced_true
577 app = lambda { |env| [302, [ %w(a) ], body ] }
578 o = { :logger => s, :format => "$request_time", :reentrant => true }
579 cl = Clogger.new(app, o)
580 @req['rack.multithread'] = false
581 status, headers, body = cl.call(@req)
585 def test_clogger_auto_reentrant_forced_false
588 app = lambda { |env| [302, [ %w(a) ], body ] }
589 o = { :logger => s, :format => "$request_time", :reentrant => false }
590 cl = Clogger.new(app, o)
591 @req['rack.multithread'] = true
592 status, headers, body = cl.call(@req)
593 assert ! cl.reentrant?
596 def test_invalid_status
599 app = lambda { |env| [ env["force.status"], [ %w(a b) ], body ] }
600 o = { :logger => s, :format => "$status" }
601 cl = Clogger.new(app, o)
602 status, headers, body = cl.call(@req.merge("force.status" => -1))
603 assert_equal -1, status
604 assert_equal "-\n", s.last
605 status, headers, body = cl.call(@req.merge("force.status" => 1000))
606 assert_equal 1000, status
607 assert_equal "-\n", s.last
608 u64_max = 0xffffffffffffffff
609 status, headers, body = cl.call(@req.merge("force.status" => u64_max))
610 assert_equal u64_max, status
611 assert_equal "-\n", s.last
614 # so we don't care about the portability of this test
615 # if it doesn't leak on Linux, it won't leak anywhere else
616 # unless your C compiler or platform is otherwise broken
617 LINUX_PROC_PID_STATUS = "/proc/self/status"
619 app = lambda { |env| [ 0, {}, [] ] }
620 clogger = Clogger.new(app, :logger => $stderr)
621 match_rss = /^VmRSS:\s+(\d+)/
622 if File.read(LINUX_PROC_PID_STATUS) =~ match_rss
624 1000000.times { clogger.dup }
625 File.read(LINUX_PROC_PID_STATUS) =~ match_rss
627 diff = after - before
628 assert(diff < 10000, "memory grew more than 10M: #{diff}")
630 end if RUBY_PLATFORM =~ /linux/ && File.readable?(LINUX_PROC_PID_STATUS)