when wrapping the body, body.close writes the log
[clogger.git] / test / test_clogger.rb
blob13110175b3428caf8a0049b910e445a856c7f4df
1 # -*- encoding: binary -*-
2 $stderr.sync = $stdout.sync = true
3 require "test/unit"
4 require "date"
5 require "stringio"
7 require "rack"
9 require "clogger"
11 # used to test subclasses
12 class FooString < String
13 end
15 class TestClogger < Test::Unit::TestCase
16   include Clogger::Format
18   def setup
19     @req = {
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',
28     }
29   end
31   def test_init_basic
32     Clogger.new(lambda { |env| [ 0, {}, [] ] })
33   end
35   def test_init_noargs
36     assert_raise(ArgumentError) { Clogger.new }
37   end
39   def test_init_stderr
40     cl = Clogger.new(lambda { |env| [ 0, {}, [] ] }, :logger => $stderr)
41     assert_kind_of(Integer, cl.fileno)
42     assert_equal $stderr.fileno, cl.fileno
43   end
45   def test_init_stringio
46     cl = Clogger.new(lambda { |env| [ 0, {}, [] ] }, :logger => StringIO.new)
47     assert_nil cl.fileno
48   end
50   def test_write_stringio
51     start = DateTime.now - 1
52     str = StringIO.new
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 }
58     body.close
59     str = str.string
60     r = %r{\Ahome - - \[[^\]]+\] "GET /hello\?goodbye=true HTTP/1.0" 302 -\n\z}
61     assert_match r, str
62     %r{\[([^\]]+)\]} =~ str
63     tmp = nil
64     assert_nothing_raised {
65       tmp = DateTime.strptime($1, "%d/%b/%Y:%H:%M:%S %z")
66     }
67     assert tmp >= start
68     assert tmp <= DateTime.now
69   end
71   def test_clen_stringio
72     start = DateTime.now - 1
73     str = StringIO.new
74     app = lambda { |env| [ 301, {'Content-Length' => '5'}, ['abcde'] ] }
75     format = Common.dup
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) }
82     str = str.string
83     r = %r{\Ahome - - \[[^\]]+\] "GET /hello\?goodbye=true HTTP/1.0" 301 5\n\z}
84     assert_match r, str
85     %r{\[([^\]]+)\]} =~ str
86     tmp = nil
87     assert_nothing_raised {
88       tmp = DateTime.strptime($1, "%d/%b/%Y:%H:%M:%S %z")
89     }
90     assert tmp >= start
91     assert tmp <= DateTime.now
92   end
94   def test_compile_ambiguous
95     cl = Clogger.new(nil, :logger => $stderr)
96     ary = nil
97     cl.instance_eval {
98       ary = compile_format(
99         '$remote_addr $$$$pid' \
100         "\n")
101     }
102     expect = [
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" ],
108       ]
109     assert_equal expect, ary
110   end
112   def test_compile_auto_newline
113     cl = Clogger.new(nil, :logger => $stderr)
114     ary = nil
115     cl.instance_eval { ary = compile_format('$remote_addr $request') }
116     expect = [
117       [ Clogger::OP_REQUEST, "REMOTE_ADDR" ],
118       [ Clogger::OP_LITERAL, " " ],
119       [ Clogger::OP_SPECIAL, Clogger::SPECIAL_VARS[:request] ],
120       [ Clogger::OP_LITERAL, "\n" ],
121       ]
122     assert_equal expect, ary
123   end
125   def test_big_log
126     str = StringIO.new
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
133     req = {
134       'HTTP_HOST' => 'example.com:12345',
135       'HTTP_COOKIE' => cookie,
136     }
137     req = @req.merge(req)
138     body = cl.call(req).last
139     body.each { |part| part }
140     body.close
141     str = str.string
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
145   end
147   def test_compile
148     cl = Clogger.new(nil, :logger => $stderr)
149     ary = nil
150     cl.instance_eval {
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}' \
156         "\n")
157     }
158     expect = [
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" ],
181     ]
182     assert_equal expect, ary
183   end
185   def test_eval
186     current = Thread.current.to_s
187     str = StringIO.new
188     app = lambda { |env| [ 302, {}, [] ] }
189     cl = Clogger.new(app,
190                     :logger => str,
191                     :format => "-$e{Thread.current}-\n")
192     status, headers, body = cl.call(@req)
193     assert_equal "-#{current}-\n", str.string
194   end
196   def test_pid
197     str = StringIO.new
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
202   end
204   def test_rack_xff
205     str = StringIO.new
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
211     str.rewind
212     str.truncate(0)
213     status, headers, body = cl.call(@req)
214     assert_equal "home\n", str.string
215     str.rewind
216     str.truncate(0)
217   end
219   def test_rack_1_0
220     start = DateTime.now - 1
221     str = StringIO.new
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)
225     tmp = []
226     body.each { |s| tmp << s }
227     body.close
228     assert_equal %w(a b c), tmp
229     str = str.string
230     assert_match %r[" 200 3 \d+\.\d{4}\n\z], str
231     tmp = nil
232     %r{\[(\d+/\w+/\d+ \d+:\d+:\d+)\]} =~ str
233     assert $1
234     assert_nothing_raised { tmp = DateTime.strptime($1, "%d/%b/%Y %H:%M:%S") }
235     assert tmp >= start
236     assert tmp <= DateTime.now
237   end
239   def test_msec
240     str = StringIO.new
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
245   end
247   def test_usec
248     str = StringIO.new
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
253   end
255   def test_time_0
256     str = StringIO.new
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
261   end
263   def test_time_1
264     str = StringIO.new
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
269   end
271   def test_request_length
272     str = StringIO.new
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
278   end
280   def test_response_length_0
281     str = StringIO.new
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 }
286     bodies.close
287     assert_equal "-\n", str.string
288   end
290   def test_combined
291     start = DateTime.now - 1
292     str = StringIO.new
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)
296     tmp = []
297     body.each { |s| tmp << s }
298     body.close
299     assert_equal %w(a b c), tmp
300     str = str.string
301     assert_match %r[" 200 3 "-" "echo and socat \\o/"\n\z], str
302     tmp = nil
303     %r{\[(\d+/\w+/\d+:\d+:\d+:\d+ .+)\]} =~ str
304     assert $1
305     assert_nothing_raised {
306       tmp = DateTime.strptime($1, "%d/%b/%Y:%H:%M:%S %z")
307     }
308     assert tmp >= start
309     assert tmp <= DateTime.now
310   end
312   def test_rack_errors_fallback
313     err = StringIO.new
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
319   end
321   def test_body_close
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?
329   end
331   def test_escape
332     str = StringIO.new
333     app = lambda { |env| [ 200, {'Content-Length'=>'5'}, [] ] }
334     cl = Clogger.new(app,
335       :logger => str,
336       :format => '$http_user_agent "$request"')
337     bad = {
338       'HTTP_USER_AGENT' => '"asdf"',
339       'QUERY_STRING' => 'sdf=bar"',
340       'PATH_INFO' => '/"<>"',
341     }
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
345   end
347   # rack allows repeated headers with "\n":
348   # { 'Set-Cookie' => "a\nb" } =>
349   #   Set-Cookie: a
350   #   Set-Cookie: b
351   def test_escape_header_newlines
352     str = StringIO.new
353     app = lambda { |env| [302, { 'Set-Cookie' => "a\nb" }, [] ] }
354     cl = Clogger.new(app, :logger => str, :format => '$sent_http_set_cookie')
355     cl.call(@req)
356     assert_equal "a\\x0Ab\n", str.string
357   end
359   def test_request_uri_fallback
360     str = StringIO.new
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
365   end
367   def test_request_uri_set
368     str = StringIO.new
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
373   end
375   def test_cookies
376     str = StringIO.new
377     app = lambda { |env|
378       req = Rack::Request.new(env).cookies
379       [ 302, {}, [] ]
380     }
381     cl = Clogger.new(app,
382         :format => '$cookie_foo $cookie_quux',
383         :logger => str)
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
387   end
389   def test_bogus_app_response
390     str = StringIO.new
391     app = lambda { |env| 302 }
392     cl = Clogger.new(app, :logger => str)
393     assert_raise(TypeError) { cl.call(@req) }
394     str = str.string
395     e = Regexp.quote " \"GET /hello?goodbye=true HTTP/1.0\" 500 -"
396     assert_match %r{#{e}$}m, str
397   end
399   def test_broken_header_response
400     str = StringIO.new
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) }
404   end
406   def test_subclass_hash
407     str = StringIO.new
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 }
412     assert str.size > 0
413   end
415   def test_subclassed_string_req
416     str = StringIO.new
417     req = {}
418     @req.each { |key,value|
419       req[FooString.new(key)] = value.kind_of?(String) ?
420                                 FooString.new(value) : value
421     }
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 }
425     assert str.size > 0
426   end
428   def test_subclassed_string_in_body
429     str = StringIO.new
430     body = "hello"
431     r = nil
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 }
435     assert str.size > 0
436     assert_equal body.size.to_s << "\n", str.string
437     assert_equal r, body
438     assert r.object_id != body.object_id
439   end
441   def test_http_09_request
442     str = StringIO.new
443     app = lambda { |env| [302, [ %w(a) ], []] }
444     cl = Clogger.new(app, :logger => str, :format => '$request')
445     req = @req.dup
446     req.delete 'HTTP_VERSION'
447     cl.call(req)
448     assert_equal "GET /hello?goodbye=true\n", str.string
449   end
451   def test_request_method_only
452     str = StringIO.new
453     app = lambda { |env| [302, [ %w(a) ], []] }
454     cl = Clogger.new(app, :logger => str, :format => '$request_method')
455     cl.call(@req)
456     assert_equal "GET\n", str.string
457   end
459   def test_content_length_null
460     str = StringIO.new
461     app = lambda { |env| [302, [ %w(a) ], []] }
462     cl = Clogger.new(app, :logger => str, :format => '$content_length')
463     cl.call(@req)
464     assert_equal "-\n", str.string
465   end
467   def test_content_length_set
468     str = StringIO.new
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
473   end
475   def test_http_content_type_fallback
476     str = StringIO.new
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
481   end
483   def test_clogger_synced
484     io = StringIO.new
485     logger = Struct.new(:sync, :io).new(false, io)
486     assert ! logger.sync
487     def logger.<<(str)
488       io << str
489     end
490     app = lambda { |env| [302, [ %w(a) ], []] }
491     cl = Clogger.new(app, :logger => logger)
492     assert logger.sync
493   end
495   def test_clogger_unsyncable
496     logger = ''
497     assert ! logger.respond_to?('sync=')
498     app = lambda { |env| [302, [ %w(a) ], []] }
499     assert_nothing_raised { Clogger.new(app, :logger => logger) }
500   end
502   def test_clogger_no_ORS
503     s = ''
504     app = lambda { |env| [302, [ %w(a) ], []] }
505     cl = Clogger.new(app, :logger => s, :format => "$request", :ORS => "")
506     cl.call(@req)
507     assert_equal "GET /hello?goodbye=true HTTP/1.0", s
508   end
510   def test_clogger_weird_ORS
511     s = ''
512     app = lambda { |env| [302, [ %w(a) ], []] }
513     cl = Clogger.new(app, :logger => s, :format => "<$request", :ORS => ">")
514     cl.call(@req)
515     assert_equal "<GET /hello?goodbye=true HTTP/1.0>", s
516   end
518   def test_clogger_body_not_closeable
519     s = ''
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
524   end
526   def test_clogger_response_frozen
527     response = [ 200, { "AAAA" => "AAAA"}.freeze, [].freeze ].freeze
528     s = StringIO.new("")
529     app = Rack::Builder.new do
530       use Clogger, :logger => s, :format => "$request_time $http_host"
531       run lambda { |env| response }
532     end
533     assert_nothing_raised do
534       3.times do
535         resp = app.call(@req)
536         assert ! resp.frozen?
537         resp.last.each { |x| }
538       end
539     end
540   end
542   def test_clogger_body_close_return_value
543     s = ''
544     body = []
545     def body.close
546       :foo
547     end
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
552   end
554   def test_clogger_auto_reentrant_true
555     s = ''
556     body = []
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)
561     assert cl.reentrant?
562   end
564   def test_clogger_auto_reentrant_false
565     s = ''
566     body = []
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?
572   end
574   def test_clogger_auto_reentrant_forced_true
575     s = ''
576     body = []
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)
582     assert cl.reentrant?
583   end
585   def test_clogger_auto_reentrant_forced_false
586     s = ''
587     body = []
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?
594   end
596   def test_invalid_status
597     s = []
598     body = []
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
612   end
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"
618   def test_memory_leak
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
623       before = $1.to_i
624       1000000.times { clogger.dup }
625       File.read(LINUX_PROC_PID_STATUS) =~ match_rss
626       after = $1.to_i
627       diff = after - before
628       assert(diff < 10000, "memory grew more than 10M: #{diff}")
629     end
630   end if RUBY_PLATFORM =~ /linux/ && File.readable?(LINUX_PROC_PID_STATUS)